Как работает оператор return в Haskell?

Рассмотрим эти функции

f1 :: Maybe Int
f1 = return 1

f2 :: [Int]
f2 = return 1

Оба имеют один и тот же оператор return 1. Но результаты разные. f1 дает значение Just 1, а f2 дает значение [1]

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


person shajen    schedule 09.01.2018    source источник
comment
Это не заявление...   -  person Willem Van Onsem    schedule 10.01.2018
comment
return в Haskell работает совершенно иначе, чем в разных языках. Притворись, что это даже не называется return. inject, вероятно, было бы немного лучшим именем, чтобы думать об этом.   -  person Carcigenicate    schedule 10.01.2018
comment
Для начала предлагаю запустить ghci и набрать :t return. (Возможно, вам придется сначала импортировать правильный модуль.) Это даст вам некоторую информацию о его типе, которая может дать вам некоторые подсказки о том, как он работает.   -  person Code-Apprentice    schedule 10.01.2018
comment
return на самом деле просто простая функция в Haskell. Он не ничего не возвращает. Он оборачивает значение в монаду.   -  person Willem Van Onsem    schedule 10.01.2018
comment
Кроме того, вы должны изучить классы типов. Здесь используется класс типа Monad.   -  person Code-Apprentice    schedule 10.01.2018
comment
Кроме того, функция не нуждается в каком-либо специальном операторе return в Haskell. Возвращаемое значение функции — это просто значение, вычисленное телом функции. Обратите внимание, что ни f1, ни f2 вообще не являются функциями; это просто значения, поскольку функция по определению принимает ровно один аргумент.   -  person chepner    schedule 10.01.2018
comment
Это можно назвать полиморфизмом классов или иногда специальным полиморфизмом (хотя настоящие специальные классы обычно не рекомендуются).   -  person dfeuer    schedule 10.01.2018
comment
Похоже, return — перегруженная функция. Обычно функция перегружается на основе типа аргумента функции, но в этом случае она перегружается на основе возвращаемого типа.   -  person shajen    schedule 10.01.2018
comment
@shajen Haskell не имеет перегруженных функций. Имеет полиморфные функции. Это похоже на реализацию интерфейса в объектно-ориентированных языках.   -  person 4castle    schedule 10.01.2018
comment
@shajen в этом случае перегружен в зависимости от возвращаемого типа. Это иногда называют полиморфизмом возвращаемого типа softwareengineering.stackexchange.com/questions/105662/ Простым примером этого является mempty для класса типов Monoid.   -  person danidiaz    schedule 10.01.2018
comment
@Carcigenicate Или просто используйте pure вместо изобретения inject, поскольку pure уже существует и является (более мощным) синонимом return.   -  person amalloy    schedule 10.01.2018
comment
@Carcigenicate Соглашусь с амаллоем здесь. В любом случае это не всегда инъекция (Proxy приводит контрпример).   -  person David Young    schedule 10.01.2018
comment
@amalloy Это справедливо. Я использовал Inject, потому что пытался придумать слово во время ходьбы, а Inject использовался в статье, которую я читал несколько лет назад на эту тему.   -  person Carcigenicate    schedule 10.01.2018


Ответы (2)


Это длинный извилистый ответ!

Как вы, наверное, поняли из комментариев и отличного (но очень технического) ответа Томаса, вы задали очень сложный вопрос. Отличная работа!

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

return – это пример выведения типов.

Большинство современных языков имеют некоторое понятие полиморфизма. Например, var x = 1 + 1 установит x равным 2. В статически типизированном языке 2 обычно будет целым числом. Если вы скажете var y = 1.0 + 1.0, то y будет числом с плавающей запятой. Оператор + (это просто функция со специальным синтаксисом)

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

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

Таким образом, в императивном языке «поток» вывода типа следует за временем в вашей программе. Выведите тип переменной, сделайте что-нибудь с ней и выведите тип результата. В динамически типизированном языке (таком как Python или javascript) тип переменной не присваивается до тех пор, пока не будет вычислено значение переменной (поэтому кажется, что типов нет). В статически типизированном языке типы обрабатываются заранее (компилятором), но логика та же. Компилятор определяет, какими будут типы переменных, но он делает это, следуя логике программы так же, как она работает.

В Haskell вывод типа также следует логике программы. Будучи Haskell, он делает это очень математически чистым способом (называемым System F). Язык типов (то есть правила, по которым выводятся типы) похож на сам Haskell.

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

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

Рассмотрим эту функцию

func (x : y : rest) = (x,y) : func rest
func _ = []

Если вы спросите Haskell о типе этой функции, он посмотрит на определение, увидит [] и : и сделает вывод, что работает со списками. Но ему никогда не нужно смотреть на типы x и y, он просто знает, что они должны быть одинаковыми, потому что они попадают в один и тот же список. Таким образом, он выводит тип функции как [a] -> [a], где a — это тип, который еще не удосужился обработать.

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

Теперь попробуйте ввести следующее в ghci

maxBound - length ""
maxBound : "Hello"

Теперь, что только что произошло!? minBound может быть Char, потому что я поместил его в начале строки, и это должно быть целое число, потому что я добавил его к 0 и получил число. Кроме того, эти два значения явно сильно различаются.

Итак, каков тип minBound? Давайте спросим ghci!

:type minBound
minBound :: Bounded a => a

Аааааа! что это значит? По сути, это означает, что он не удосужился точно определить, что такое a, но это должно быть Bounded, если вы наберете :info Bounded, вы получите три полезные строки.

class Bounded a where
  minBound :: a
  maxBound :: a

и много менее полезных строк

Итак, если a равно Bounded, существуют значения minBound и maxBound типа a. На самом деле под капотом Bounded — это просто значение, его «тип» — это запись с полями minBound и maxBound. Поскольку это значение, Haskell не смотрит на него до тех пор, пока это действительно не нужно.

Так что я, кажется, блуждал где-то в районе ответа на ваш вопрос. Прежде чем мы перейдем к return (который, как вы, возможно, заметили из комментариев, является удивительно сложным зверем), давайте посмотрим на read.

ghci снова

read "42" + 7
read "'H'" : "ello"
length (read "[1,2,3]")

и, надеюсь, вы не будете слишком удивлены, обнаружив, что существуют определения

read :: Read a => String -> a
class Read where
  read :: String -> a

поэтому Read a - это просто запись, содержащая одно значение, которое является функцией String -> a. Очень заманчиво предположить, что есть одна функция чтения, которая просматривает строку, определяет, какой тип содержится в строке, и возвращает этот тип. Но это делает наоборот. Он полностью игнорирует строку, пока она не понадобится. Когда значение необходимо, Haskell сначала выясняет, какой тип он ожидает, и как только это сделано, он идет и получает соответствующую версию функции чтения и объединяет ее со строкой.

теперь рассмотрим что-то немного более сложное

readList :: Read a => [String] -> a
readList strs = map read strs

под капотом readList фактически принимает два аргумента readList' (Read a) -> [String] -> [a] readList' {read = f} strs = map f strs

Опять же, поскольку Haskell ленив, он утруждает себя просмотром аргументов только тогда, когда ему нужно узнать возвращаемое значение, в этот момент он знает, что такое a, поэтому компилятор может пойти и определить правильную версию Read. А пока это не волнует.

Надеюсь, это дало вам некоторое представление о том, что происходит и почему Haskell может "перегружаться" по возвращаемому типу. Но важно помнить, что это не перегрузка в обычном смысле. Каждая функция имеет только одно определение. Просто один из аргументов - это мешок функций. read_str никогда не знает, с какими типами он имеет дело. Он просто знает, что получает функцию String -> a и несколько строк, чтобы выполнить приложение, он просто передает аргументы map. map, в свою очередь, даже не знает, что получает строки. Когда вы углубляетесь в Haskell, становится очень важным, чтобы функции мало знали о типах, с которыми они имеют дело.

Теперь давайте посмотрим на return.

Помните, я говорил, что система типов в Haskell очень похожа на сам Haskell. Помните, что в Haskell функции — это обычные значения. Означает ли это, что у меня может быть тип, который принимает тип в качестве аргумента и возвращает другой тип? Конечно!

Вы видели, что некоторые функции типов Maybe принимают тип a и возвращают другой тип, который может быть либо Just a, либо Nothing. [] принимает тип a и возвращает список as. Типовые функции в Haskell обычно являются контейнерами. Например, я мог бы определить функцию типа BinaryTree, которая хранит загрузку a в древовидной структуре. Есть, конечно, много гораздо более странных.

Итак, если эти функции типа похожи на обычные типы, у меня может быть класс типов, содержащий функции типов. Одним из таких классов типов является Monad.

class Monad m where
  return a -> m a
  (>>=) m a (a -> m b) -> m b

так что здесь m - это некоторая функция типа. Если я хочу определить Monad для m, мне нужно определить return и устрашающе выглядящий оператор под ним (который называется bind)

Как уже отмечали другие, return - это действительно вводящее в заблуждение имя для довольно скучной функции. Команда, разработавшая Haskell, осознала свою ошибку и искренне сожалеет об этом. return — это обычная функция, которая принимает аргумент и возвращает Monad с этим типом. (Вы никогда не спрашивали, что такое монада на самом деле, поэтому я не буду вам говорить)

Давайте определим Monad для m = Maybe! Сначала мне нужно определить return. Каким должно быть return x? Помните, мне разрешено определять функцию только один раз, поэтому я не могу смотреть на x, потому что не знаю, какого она типа. Я всегда мог бы вернуть Nothing, но это кажется пустой тратой совершенно хорошей функции. Давайте определим return x = Just x, потому что это буквально единственное, что я могу сделать.

А как насчет страшной привязки? что мы можем сказать о x >>= f? Ну, x — это Maybe a неизвестного типа a, а f — это функция, которая принимает a и возвращает Maybe b. Каким-то образом мне нужно объединить их, чтобы получить «Может быть, б`

Поэтому мне нужно определить Nothing >== f. Я не могу вызвать f, потому что ему нужен аргумент типа a, а у меня нет значения типа a. Я даже не знаю, что такое "а". У меня есть только один выбор, который должен определить

Nothing >== f = Nothing

А Just x >>= f? Ну, я знаю, что x имеет тип a, а f принимает a в качестве аргумента, поэтому я могу установить y = f a и сделать вывод, что y имеет тип b. Теперь мне нужно сделать Maybe b, а у меня есть b, так что...

Просто x >>= f = Просто (f x)

Итак, у меня есть Monad! что если m это List? хорошо, я могу следовать аналогичной логике и определить

return x = [x]
[] >>= f = []
(x : xs) >>= a = f x ++ (xs >>= f)

Ура еще Monad! Это хорошее упражнение — пройтись по шагам и убедить себя, что нет другого разумного способа определить это.

Так что же происходит, когда я звоню return 1?

Ничего такого!

Ленивый помните Haskell. преобразователь return 1 (технический термин) просто находится там до тех пор, пока кому-то не понадобится значение. Как только Haskell нужно значение, он знает, какого типа оно должно быть. В частности, можно сделать вывод, что m есть List. Теперь, когда он знает, что Haskell может найти экземпляр Monad для List. Как только он это сделает, он получит доступ к правильной версии return.

Итак, наконец, Haskell готов вызвать return, который в данном случае возвращает [1]!

person Tim    schedule 10.01.2018
comment
Итак, Haskell применяет две вещи в этой ситуации. Сначала он делает вывод о типе, а затем ищет монаду, в которой реализован «возврат» для этого типа. Думаю, то же самое происходит и в этих примерах: fromJust (возврат 1) и head (возврат 1). Спасибо за объяснение. Ответ Томаса объяснил, как существует несколько версий «возврата» - person shajen; 12.01.2018

Функция return относится к классу Monad:

class Applicative m => Monad (m :: * -> *) where
  ...
  return :: a -> m a

Таким образом, return принимает любое значение типа a и возвращает значение типа m a. Монада m, как вы заметили, является полиморфной, используя класс типов Haskell Monad для специального полиморфизма.

В этот момент вы, вероятно, понимаете, что return не является хорошим, интуитивно понятным именем. Это даже не встроенная функция или оператор, как во многих других языках. На самом деле существует функция с более удачным названием и идентично работающая — pure. Почти во всех случаях return = pure.

То есть функция return такая же, как и функция pure (из класса Applicative) - я часто думаю про себя "это монадическое значение является чисто лежащим в основе a" и стараюсь использовать чистое значение вместо возврата, если его еще нет соглашение в кодовой базе.

Вы можете использовать return (или чистый) для любого типа, который является классом Monad. Это включает монаду Maybe для получения значения типа Maybe a:

instance Monad Maybe where
...
    return      = pure  -- which is from Applicative

...
instance Applicative Maybe where
    pure = Just

Или чтобы монада списка получила значение [a]:

instance Applicative [] where
    {-# INLINE pure #-}
    pure x    = [x]

Или, как более сложный пример, монада синтаксического анализа Aeson для получения значения типа Parser a:

instance Applicative Parser where
    pure a = Parser $ \_path _kf ks -> ks a
person Thomas M. DuBuisson    schedule 09.01.2018