Что означает восклицательный знак в декларации Haskell?

262

Я натолкнулся на следующее определение, пытаясь изучить Haskell, используя для этого реальный проект. Я не понимаю, что означает восклицательный знак перед каждым аргументом, и мои книги, похоже, не упоминали об этом.

data MidiMessage = MidiMessage !Int !MidiMessage
Дэвид
источник
13
Я подозреваю, что это может быть очень распространенным вопросом; Я точно помню, что сам задавался вопросом о том же самом, когда-то.
cjs

Ответы:

314

Это декларация строгости. По сути, это означает, что при создании значения структуры данных оно должно быть оценено как «нормальная форма слабой головы». Давайте рассмотрим пример, чтобы понять, что это значит:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

Приведенная fвыше функция при оценке вернет «thunk»: код, который нужно выполнить, чтобы выяснить его значение. На данный момент, Foo даже еще не существует, только код.

Но в какой-то момент кто-то может попытаться заглянуть внутрь него, возможно, с помощью сопоставления с образцом:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Это будет выполнять достаточно кода, чтобы делать то, что ему нужно, и не более того. Таким образом, он создаст Foo с четырьмя параметрами (потому что вы не можете заглянуть внутрь него, если он не существует). Во-первых, так как мы тестируем его, нам нужно оценить весь путь туда 4, где мы понимаем, что он не соответствует.

Второе не нужно оценивать, потому что мы его не тестируем. Таким образом, вместо того, 6чтобы храниться в этой ячейке памяти, мы просто сохраним код для возможной последующей оценки (3+3). Это превратится в 6, только если кто-то смотрит на это.

Третий параметр, однако, имеет !перед ним, поэтому строго оценивается: (4+4)выполняется и 8хранится в этой ячейке памяти.

Четвертый параметр также строго оценен. Но вот где это становится немного сложнее: мы оцениваем не полностью, а только для слабой нормальной формы головы. Это означает, что мы выясняем, является ли это Nothingили Justчто-то, и сохраняем это, но мы не идем дальше. Это означает, что мы храним не так, Just 10а на самом деле Just (5+5), оставляя thunk внутри неоцененным. Это важно знать, хотя я думаю, что все последствия этого выходят за рамки этого вопроса.

Вы можете аннотировать аргументы функции таким же образом, если вы включите BangPatternsрасширение языка:

f x !y = x*y

f (1+1) (2+2)вернет гром (1+1)*4.

CJS
источник
16
Это очень полезно. Однако я не могу не задаться вопросом, не усложняет ли терминология на Хаскеле вещи для людей с такими терминами, как «слабая нормальная форма головы», «строгий» и так далее. Если я вас правильно понимаю, это звучит как! Оператор просто означает сохранение оцененного значения выражения, а не сохранение анонимного блока, чтобы оценить его позже. Это разумное толкование или есть что-то еще?
Дэвид
71
@ Давид Вопрос в том, как далеко оценить ценность. Обычная форма слабой головы означает: оценивать ее до тех пор, пока не дойдете до самого внешнего конструктора. Нормальная форма означает оценку всего значения до тех пор, пока не останется никаких оцененных компонентов. Поскольку Haskell допускает всевозможные уровни глубины оценки, он имеет богатую терминологию для описания этого. Вы не склонны искать эти различия в языках, которые поддерживают только семантику вызовов по значению.
Дон Стюарт
8
@ Дэвид: Я написал более подробное объяснение здесь: Haskell: Что такое нормальная форма слабой головы? , Хотя я не упоминаю шаблоны взрыва или аннотации строгости, они эквивалентны использованию seq.
Хаммар
1
Просто чтобы убедиться, что я понял: относится ли эта лень только к неизвестным во время компиляции аргументам? Т.е. если в исходном коде действительно есть оператор (2 + 2) , будет ли он все равно оптимизирован до 4 вместо кода для добавления известных чисел?
Привет, Ангел,
1
Привет, Ангел, это действительно зависит от того, насколько далеко компилятор хочет оптимизировать. Хотя мы используем удары, чтобы сказать, что мы хотели бы заставить определенные вещи ослаблять нормальную форму головы, у нас нет (и при этом мы не нуждаемся или не хотим) способ сказать: «Пожалуйста, убедитесь, что вы еще не оценили этот огромный». шаг вперед. " С семантической точки зрения, учитывая "n = 2 + 2", вы даже не можете сказать , оценивается ли какая-либо конкретная ссылка на n сейчас или была оценена ранее.
CJS
92

Простой способ увидеть разницу между строгими и нестрогими аргументами конструктора - это то, как они ведут себя, когда они не определены. Дано

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Поскольку не строгий аргумент не оценивается second, передача undefinedне вызывает проблемы:

> second (Foo undefined 1)
1

Но строгого аргумента быть не может undefined, даже если мы не используем значение:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Крис Конвей
источник
2
Это лучше, чем ответ Курта Сэмпсона, потому что он фактически объясняет наблюдаемые пользователем эффекты, которые !имеет символ, а не углубляется во внутренние детали реализации.
Дэвид Грейсон
26

Я считаю, что это аннотация строгости.

Haskell - это чистый и ленивый функциональный язык, но иногда накладные расходы лени могут быть слишком большими или расточительными. Таким образом, чтобы справиться с этим, вы можете попросить компилятор полностью оценить аргументы функции вместо того, чтобы разбирать thunks вокруг.

На этой странице больше информации: Производительность / Строгость .

Крис Вест
источник
Хм, пример на той странице, на которую вы ссылались, похоже, говорит об использовании! при вызове функции. Но это выглядит иначе, чем помещать его в объявление типа. Чего мне не хватает?
Дэвид
Создание экземпляра типа также является выражением. Вы можете думать о конструкторах типов как о функциях, которые возвращают новые экземпляры указанного типа, учитывая предоставленные аргументы.
Крис Вест
4
Фактически, для всех намерений и целей конструкторы типов являются функциями; Вы можете частично применить их, передать их другим функциям (например, map Just [1,2,3]чтобы получить [Just 1, Just 2, Just 3]) и так далее. Я нахожу полезным подумать о способности сопоставления с образцом с ними, а также о совершенно не связанном объекте.
cjs