Что означает синтаксис «Just» в Haskell?


118

Я поискал в Интернете фактическое объяснение того, что делает это ключевое слово. Каждый учебник по Haskell, который я просматривал, просто начинает использовать его случайным образом и никогда не объясняет, что он делает (а я просмотрел многих).

Вот базовый фрагмент кода из Real World Haskell, который использует Just. Я понимаю, что делает код, но не понимаю, какова его цель или функция Just.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Из того, что я наблюдал, это связано с Maybeнабором текста, но это почти все, что мне удалось узнать.

Было Justбы очень полезно хорошее объяснение того, что означает.

Ответы:


211

На самом деле это обычный конструктор данных, который определяется в Prelude , стандартной библиотеке, которая автоматически импортируется в каждый модуль.

Что может быть, структурно

Определение выглядит примерно так:

data Maybe a = Just a
             | Nothing

Это объявление определяет тип, Maybe aкоторый параметризуется переменной типа a, что означает лишь то, что вы можете использовать его с любым типом вместо a.

Конструирование и разрушение

Тип имеет два конструктора, Just a и Nothing. Если тип имеет несколько конструкторов, это означает, что значение типа должно быть построено только с одним из возможных конструкторов. Для этого типа значение было либо построено с помощью, Justлибо Nothing, других возможностей (без ошибок) нет.

Поскольку Nothingне имеет типа параметра, когда он используется в качестве конструктора, он называет постоянное значение, которое является членом типа Maybe aдля всех типов.a . Но у Justконструктора есть параметр типа, что означает, что при использовании в качестве конструктора он действует как функция от типа aк Maybe a, то есть имеет типa -> Maybe a

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

Чтобы использовать Maybe aзначение в сопоставлении с шаблоном, вам необходимо предоставить шаблон для каждого конструктора, например:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

В этом случае выражение, первый шаблон будет соответствовать, если значение было Nothing, и второй будет соответствовать, если значение было построено с Just. Если второй совпадает, он также связывает имяval с параметром, который был передан Justконструктору при построении значения, с которым вы сопоставляете.

Что может означать

Возможно, вы уже были знакомы с тем, как это работает; в значениях нет никакого волшебства Maybe, это просто обычный алгебраический тип данных Haskell (ADT). Но он используется довольно часто, потому что он эффективно «поднимает» или расширяет тип, например, Integerиз вашего примера, в новый контекст, в котором он имеет дополнительное значение ( Nothing), которое представляет собой отсутствие значения! Затем система типов требует, чтобы вы проверили это дополнительное значение, прежде чем оно позволит вам найти то, Integerчто может быть там. Это предотвращает значительное количество ошибок.

Многие языки сегодня обрабатывают такого рода значения «без значения» через ссылки NULL. Тони Хоар, выдающийся ученый-компьютерщик (он изобрел Quicksort и является лауреатом премии Тьюринга), считает это своей «ошибкой на миллиард долларов» . Тип Maybe - не единственный способ исправить это, но он оказался эффективным способом сделать это.

Может быть как функтор

Идея преобразования одного типа в другой таким образом, чтобы операции со старым типом также могли быть преобразованы для работы с новым типом, лежит в основе вызываемого класса типов Haskell Functor, который Maybe aимеет полезный экземпляр.

Functorпредоставляет вызываемый метод fmap, который отображает функции, которые варьируются от значений базового типа (например, Integer) до функций, которые варьируются от значений из поднятого типа (например, Maybe Integer). Функция, преобразованная fmapдля работы со Maybeзначением, работает следующим образом:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Итак, если у вас есть Maybe Integerзначение m_xи Int -> Intфункция f, вы можете fmap f m_xприменить эту функцию fнепосредственно к Maybe Integerобъекту, не беспокоясь о том, действительно ли оно имеет значение или нет. Фактически, вы можете применить к значениям целую цепочку расширенных Integer -> Integerфункций, Maybe Integerи вам нужно будет только беспокоиться о явной проверке Nothingтолько один раз, когда вы закончите.

Может быть, как монада

Я еще не уверен, насколько вы знакомы с концепцией a Monad, но вы, по крайней мере, использовали его IO aраньше, и подпись типа IO aвыглядит удивительно похожей на Maybe a. Несмотря IOна то, что он особенный в том, что он не предоставляет вам свои конструкторы и, таким образом, может «запускаться» только системой времени выполнения Haskell, он по-прежнему является не Functorтолько файлом Monad. На самом деле, есть важное значение, в котором a Monad- это просто особый видFunctor с некоторыми дополнительными функциями, но здесь не место для этого.

В любом случае, монады любят IOсопоставлять типы с новыми типами, которые представляют «вычисления, которые приводят к значениям», и вы можете преобразовывать функции в Monadтипы с помощью очень fmapпохожей функции, называемой liftM которая превращает обычную функцию в «вычисление, которое приводит к значению, полученному путем оценки функция «.

Вы, наверное, догадались (если дочитали до этого места), что Maybeэто тоже Monad. Он представляет собой «вычисления, которые могут не вернуть значение». Как и в fmapпримере, это позволяет вам выполнять целый ряд вычислений без необходимости явно проверять наличие ошибок после каждого шага. Фактически, при построении Monadэкземпляра вычисление Maybeзначений останавливается, как только Nothingвстречается a , так что это похоже на немедленное прерывание или возврат без значения в середине вычисления.

Вы могли бы написать "Может быть"

Как я уже говорил ранее, Maybeтипу не присуще ничего, что встроено в синтаксис языка или систему времени выполнения. Если бы Haskell не предоставлял его по умолчанию, вы могли бы предоставить все его функции самостоятельно! Фактически, вы все равно можете написать его снова, с другими именами, и получить ту же функциональность.

Надеюсь, Maybeтеперь вы понимаете этот тип и его конструкторы, но если все еще что-то неясно, дайте мне знать!


19
Какой выдающийся ответ! Следует упомянуть, что Haskell часто использует Maybeтам, где другие языки используют nullor nil(с неприятными NullPointerExceptionсловами, скрывающимися в каждом углу). Теперь другие языки также начинают использовать эту конструкцию: Scala as Option, и даже Java 8 будет иметь этот Optionalтип.
Landei

3
Это отличное объяснение. Многие объяснения, которые я прочитал, как бы намекают на идею, что Just является конструктором для типа Maybe, но ни одно из них никогда не делало этого явным.
reem

@Landei, спасибо за предложение. Я внес правку, чтобы указать на опасность нулевых ссылок.
Леви Пирсон

@Landei Тип option существует с ML в 70-х, скорее всего, Scala заимствовал его оттуда, потому что Scala использует соглашение об именах ML, Option с конструкторами some и none.
Stonemetal

@Landei Apple Swift также довольно часто использует дополнительные опции
Джамин

37

Большинство текущих ответов - это технические объяснения того, как Justработают друзья; Я подумал, что могу попытаться объяснить, для чего это нужно.

Многие языки имеют такое значение, nullкоторое можно использовать вместо реального значения, по крайней мере, для некоторых типов. Это очень рассердило многих людей и считается плохим ходом. Тем не менее, иногда полезно иметь значение, например, nullчтобы указать на отсутствие вещи.

Haskell решает эту проблему, заставляя вас явно отмечать места, где у вас может быть Nothing(его версия null). По сути, если ваша функция обычно возвращает тип Foo, она должна возвращать тип Maybe Foo. Если вы хотите указать, что значения нет, вернитесь Nothing. Если вы хотите вернуть значение bar, вам следует вернуться Just bar.

В общем, если вы не можете иметь Nothing, вам и не нужно Just. Если можно Nothing, значит, нужно Just.

В этом нет ничего волшебного Maybe; он построен на системе типов Haskell. Это означает, что вы можете использовать с ним все обычные приемы сопоставления с образцом Haskell .


1
Очень хороший ответ рядом с другими ответами, но я думаю, что он все равно выиграет от примера кода :)
PascalVKooten

13

Для данного типа tзначение Just t- это существующее значение типа t, где Nothingпредставляет собой неспособность достичь значения или случай, когда наличие значения было бы бессмысленным.

В вашем примере отрицательный баланс не имеет смысла, поэтому, если такое произойдет, он будет заменен на Nothing.

В другом примере это можно использовать при делении, определяя функцию деления, которая принимает aи bи возвращает, Just a/bесли не bравно нулю, и в Nothingпротивном случае. Это часто используется в качестве удобной альтернативы исключениям или, как в предыдущем примере, для замены значений, которые не имеют смысла.


1
Итак, допустим, в приведенном выше коде я удалил Just, почему это не сработает? Вы должны иметь Just в любое время, когда вы хотите иметь Ничто? Что происходит с выражением, в котором функция внутри этого выражения ничего не возвращает?
reem

7
Если вы удалите Just, ваш код не будет проверять типы. Причина в том Just, чтобы поддерживать правильные типы. Существует тип (на самом деле монада, но его легче представить как простой тип) Maybe t, который состоит из элементов формы Just tи Nothing. Поскольку Nothingимеет тип Maybe t, выражение, которое может оценивать одно Nothingили какое-то значение типа t, не типизировано должным образом. Если функция возвращает Nothingв некоторых случаях, любое выражение, использующее эту функцию, должно иметь какой-то способ проверки этого ( isJustили оператор case), чтобы обрабатывать все возможные случаи.
qaphla

2
Так что Just просто существует для согласованности в типе Maybe, потому что обычного t нет в типе Maybe. Теперь все намного яснее. Спасибо!
reem

3
@qaphla: Ваш комментарий о том, что это «монада, на самом деле [...]», вводит в заблуждение. Maybe t это просто типаж. Тот факт, что существует Monadэкземпляр для Maybe, не превращает его во что-то, что не является типом.
Сара

2

Полная функция a-> b может найти значение типа b для каждого возможного значения типа a.

В Haskell не все функции являются тотальными. В данном конкретном случае функция lendне является итоговой - она ​​не определена для случая, когда баланс меньше резерва (хотя, на мой вкус, было бы разумнее не разрешать newBalance быть меньше резерва - как есть, вы можете взять 101 из баланс 100).

Другие конструкции, которые имеют дело с неполными функциями:

  • выбрасывать исключения при проверке входного значения не соответствует диапазону
  • вернуть специальное значение (примитивный тип): избранное - отрицательное значение для целочисленных функций, которые предназначены для возврата натуральных чисел (например, String.indexOf - когда подстрока не найдена, возвращаемый индекс обычно считается отрицательным)
  • вернуть специальное значение (указатель): NULL или подобное
  • молча вернуть, ничего не делая: например, lendможно написать, чтобы вернуть старый баланс, если условие для кредитования не выполнено
  • вернуть специальное значение: Ничего (или Левый перенос объекта описания ошибки)

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

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

Проблема с тихим отбрасыванием сбоя также очевидна - вы ограничиваете то, что вызывающий может делать с функцией. Например, если был lendвозвращен старый баланс, вызывающий абонент не может узнать, изменился ли баланс. Это может быть или не быть проблемой, в зависимости от предполагаемой цели.

Решение Haskell заставляет вызывающего частичную функцию иметь дело с типом, подобным Maybe aили Either error aиз-за типа возвращаемого функцией.

Таким образом, lendкак это определено, функция, которая не всегда вычисляет новый баланс - для некоторых обстоятельств новый баланс не определяется. Мы сигнализируем об этом обстоятельстве вызывающей стороне, либо возвращая специальное значение Nothing, либо заключая новый баланс в Just. Теперь у вызывающего абонента есть свобода выбора: либо обработать отказ по ссуде особым образом, либо игнорировать и использовать старый баланс - например maybe oldBalance id $ lend amount oldBalance,.


-1

Функция if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)должна иметь одинаковый тип ifTrueи ifFalse.

Итак, когда мы пишем then Nothing, мы должны использовать Maybe aвводelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type

1
Я уверен, что вы хотели сказать здесь что-то действительно глубокое
см.

1
В Haskell есть вывод типов. Нет необходимости указывать тип Nothingи Just newBalanceявно.
правый фолд

Для этого объяснения непосвященным, явные типы поясняют значение использования Just.
Филип
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.