Это длинный извилистый ответ!
Как вы, наверное, поняли из комментариев и отличного (но очень технического) ответа Томаса, вы задали очень сложный вопрос. Отличная работа!
Вместо того, чтобы пытаться объяснить технический ответ, я попытался дать вам общий обзор того, что 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
returnв Haskell работает совершенно иначе, чем в разных языках. Притворись, что это даже не называетсяreturn.inject, вероятно, было бы немного лучшим именем, чтобы думать об этом. - person Carcigenicate   schedule 10.01.2018:t return. (Возможно, вам придется сначала импортировать правильный модуль.) Это даст вам некоторую информацию о его типе, которая может дать вам некоторые подсказки о том, как он работает. - person Code-Apprentice   schedule 10.01.2018returnна самом деле просто простая функция в Haskell. Он не ничего не возвращает. Он оборачивает значение в монаду. - person Willem Van Onsem   schedule 10.01.2018Monad. - person Code-Apprentice   schedule 10.01.2018f1, ниf2вообще не являются функциями; это просто значения, поскольку функция по определению принимает ровно один аргумент. - person chepner   schedule 10.01.2018memptyдля класса типовMonoid. - person danidiaz   schedule 10.01.2018pureвместо изобретенияinject, посколькуpureуже существует и является (более мощным) синонимомreturn. - person amalloy   schedule 10.01.2018Proxyприводит контрпример). - person David Young   schedule 10.01.2018