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

Я просмотрел интернет для фактического объяснения того, что делает это ключевое слово. Каждый учебник 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, было бы очень оценено.

Ответ 1

На самом деле это обычный конструктор данных, который определен в 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 один раз, когда вы закончите.

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

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

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

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

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

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

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

Ответ 2

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

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

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

Так что в принципе, если у вас не может быть Nothing, вам не нужно Just. Если вы можете иметь Nothing, вам нужно Just.

Ничего волшебного в Maybe; он построен на системе типа Haskell. Это означает, что вы можете использовать все обычные Haskell шаблон, соответствующий трюкам с ним.

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

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

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

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

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

Ответ 5

Функция 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