Я понимаю, что потенциально этот вопрос может быть сочтен субъективным или, возможно, не по теме, поэтому я надеюсь, что вместо того, чтобы закрыть его, он будет перенесен, возможно, в раздел "Программисты".
Я начинаю изучать Haskell, в основном для собственного назидания, и мне нравятся многие идеи и принципы, лежащие в основе языка. Я увлекся функциональными языками после занятий по теории языка, где мы играли с Lisp, и я слышал много хорошего о том, насколько продуктивным может быть Haskell, поэтому я решил изучить его сам. Пока что язык мне нравится, за исключением одной вещи, от которой я никак не могу избавиться: эти чертовы подписи функций.
Мой профессиональный опыт в основном связан с OO, особенно в Java. Большинство мест, в которых я работал, вбивали в меня множество стандартных современных догм: Agile, Clean Code, TDD и т.д. После нескольких лет работы таким образом, это определенно стало моей зоной комфорта; особенно идея о том, что "хороший" код должен быть самодокументирующимся. Я привык работать в IDE, где длинные и многословные имена методов с очень описательными подписями не являются проблемой благодаря интеллектуальному автозавершению и огромному количеству аналитических инструментов для навигации по пакетам и символам; если я могу нажать Ctrl+Space в Eclipse, а затем вывести, что делает метод, глядя на его имя и локально скопированные переменные, связанные с его аргументами, вместо того, чтобы поднимать JavaDocs, я счастлив, как свинья в какашках.
Это определенно не является частью лучших практик сообщества в Haskell. Я прочитал множество различных мнений по этому вопросу, и я понимаю, что сообщество Haskell считает лаконичность "за". Я изучил How To Read Haskell, и я понимаю обоснование многих решений, но это не значит, что они мне нравятся; имена переменных из одной буквы и т.д. меня не радуют. Я признаю, что мне придется привыкнуть к этому, если я хочу продолжать работать с этим языком.
Но я не могу смириться с сигнатурами функций. Вот пример, взятый из Learn you a Haskell[...]'раздела о синтаксисе функций:
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
Я понимаю, что это глупый пример, созданный только для объяснения охранников и ограничений класса, но если бы вы изучили только сигнатуру этой функции, вы бы понятия не имели, какой из ее аргументов должен быть весом или высотой. Даже если бы вы использовали Float
или Double
вместо любого типа, это все равно не было бы сразу заметно.
Сначала я подумал, что буду милым, умным и гениальным и попытаюсь подделать это, используя более длинные имена переменных типов с несколькими ограничениями класса:
bmiTell :: (RealFloat weight, RealFloat height) => weight -> height -> String
Это выдает ошибку (в качестве отступления, если кто-то может объяснить мне эту ошибку, я буду благодарен):
Could not deduce (height ~ weight)
from the context (RealFloat weight, RealFloat height)
bound by the type signature for
bmiTell :: (RealFloat weight, RealFloat height) =>
weight -> height -> String
at example.hs:(25,1)-(27,27)
`height' is a rigid type variable bound by
the type signature for
bmiTell :: (RealFloat weight, RealFloat height) =>
weight -> height -> String
at example.hs:25:1
`weight' is a rigid type variable bound by
the type signature for
bmiTell :: (RealFloat weight, RealFloat height) =>
weight -> height -> String
at example.hs:25:1
In the first argument of `(^)', namely `height'
In the second argument of `(/)', namely `height ^ 2'
In the first argument of `(<=)', namely `weight / height ^ 2'
Не понимая до конца, почему это не сработало, я начал гуглить, и даже нашел этот небольшой пост, в котором предлагаются именованные параметры, а именно spoofing named parameters via newtype
, но это, кажется, немного перебор.
Неужели не существует приемлемого способа создания информативных сигнатур функций? Неужели "Путь Хаскеля" заключается в том, чтобы просто хаддочить все подряд?
Подпись типа не на Java-фирменный стиль. В Java-фирменный стиль будет сказать вам, какой параметр вес и высота только потому, что он смешивается имена параметров с типами параметров. Хаскелл можете'т сделать это, как правило, потому, что функции определяются с помощью сопоставления с образцом и несколько уравнений, например:
map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []
Здесь первый параметр называется Ф
в первое уравнение и _
(что означает "безымянная" - а) во второй. Второй параметр не'Т У имя в уравнение; в первой части его названия (и программист, вероятно, думать о нем, как "хз списке и"), а во втором'ы совершенно буквальное выражение.
А потом там's пункт-бесплатные определения, как:
concat :: [[a]] -> [a]
concat = foldr (++) []
Подпись типа говорит нам, он принимает параметр, который имеет тип `[[А]], но нет имени для этого параметра появляется везде системы.
За отдельное уравнение для функции, имена он использует для обозначения его аргументы не имеют значения, в любом случае Кроме в качестве документации. Поскольку идея о том, что "каноническим именем" функция's параметр это'т хорошо определены в Haskell, место для информации "в первый параметр bmiTell
является весом, в то время как второй представляет Высота!" это в документации, а не в подпись типа.
Я абсолютно согласен, что функция не должна быть кристально чистой С в "общественных" по имеющейся информации о нем. В Java, что функция's имя, а параметр типы и имена. Если (как обычно) пользователю понадобится больше информации, вы добавляете его в документации. В Haskell публичные сведения о функция's имя и типы параметров. Если пользователю потребуется больше информации, вы добавляете его в документации. Обратите внимание Иды для Хаскелла, таких как Leksah легко показать вам комментарии пикши.
Обратите внимание, что предпочитаемый вещь, чтобы сделать в языке с сильной и выразительной системой типов, как Хаскелл's часто старайтесь сделать как можно больше ошибок, как это возможно обнаружить как ошибки. Таким образом, функция как bmiTell
немедленно отправляется предупреждающие знаки для меня, по следующим причинам:
[а]
аргументы ++
делать)Единственное, что часто делается для увеличения безопасности тип действительно сделать newtypes, как в ссылке, которую вы нашли. Я не'т действительно думаю об этом, как имеющих много общего с имени параметра мимоходом, что речь идет о принятии DataType, который явно представляет высота, а не любое другое количество, которое вы, возможно, захотите, чтобы измерить количество. Так что я бы'Т есть типа значения появляются только на звонок, я бы с помощью нового типа значения там, где у меня есть данные по высоте от*, а также, и передавать его по как высота данных, а не число, так что я вам типа-безопасности (и документации) благо везде. Я бы только разверну значение в сырьевой числа, когда мне нужно его сдать что-то, что работает на количество, а не на высоте (например, арифметические операции внутри bmiTell
).
Обратите внимание, что это не имеет никакого накладными расходами; newtypes представлены так же сведения, что "внутри" в обертке типа, так что завернуть/развернуть операции нет-ОПС на базовых представлений и просто удаляются во время компиляции. Это только добавляет лишние символы в исходном коде, но эти символы ровно документация, которую вы'вновь ищет, с дополнительным преимуществом, выполняются компилятором; в стиле Java подписей сказать вам, какой параметр вес и по высоте, но компилятор по-прежнему выиграла'т быть в состоянии сказать, если вы случайно сдал их по неверному пути!
Есть и другие варианты, в зависимости от того, как глупо и/или педантичный вы хотите сделать с вашими типами.
Например, вы можете сделать это...
type Meaning a b = a
bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String
bmiTell weight height = -- etc.
...но, что's, невероятно глупо, потенциально запутанным, и не'т помочь в большинстве случаев. Тот же самый, что дополнительно требует использования расширения языка:
bmiTell :: (RealFloat weight, RealFloat height, weight ~ height)
=> weight -> height -> String
bmiTell weight height = -- etc.
Немного более разумным был бы такой:
type Weight a = a
type Height a = a
bmiTell :: (RealFloat a) => Weight a -> Height a -> String
bmiTell weight height = -- etc.
...но, что's по-прежнему глупо и, как правило, теряются, когда с GHC расширяет синонимы типа.
Реальная проблема здесь заключается в том, что вы'вновь приложив дополнительное смысловое содержание для различных значений одного полиморфного типа, который идет против зерна самого языка и, как таковая, обычно не идиоматические.
Один вариант, конечно, просто заниматься неинформативные переменные типа. Но, что's не очень приятно, если там'ы существенное различие между двумя вещами того же типа, что's не очевидно из того, что они'вновь дали слабину.
То, что я'd рекомендую вам попробовать, вместо этого используя типа
фантики для указания семантики:
newtype Weight a = Weight { getWeight :: a }
newtype Height a = Height { getHeight :: a }
bmiTell :: (RealFloat a) => Weight a -> Height a -> String
bmiTell (Weight weight) (Height height)
Делать это не так часто, как заслуживает, я думаю. Это'ы лишних нажатий на клавиатуре (ха-ха) но не только это делает ваш тип подписи более информативным, даже с Тип синонимы расширяется, это позволяет тип контролера поймать, если вы по ошибке использовать веса, высоты, или такие. С расширение GeneralizedNewtypeDeriving
вы можете даже получить автоматические экземпляры даже для классов типа, который может'т, как правило, быть получены.
Хаддоки и/или также просмотр уравнения функции (имена, к которым вы привязываете параметры) - это способы, с помощью которых я определяю, что происходит. Вы можете привязать отдельные параметры, например, так,
bmiTell :: (RealFloat a) => a -- ^ your weight
-> a -- ^ your height
-> String -- ^ what I'd think about that
так что это не просто куча текста, объясняющего все эти вещи.
Причина, по которой ваши милые переменные типа не работают, заключается в том, что ваша функция:
(RealFloat a) => a -> a -> String
Но ваша попытка изменилась:
(RealFloat weight, RealFloat height) => weight -> height -> String
эквивалентна этому:
(RealFloat a, RealFloat b) => a -> b -> String
Итак, в этой сигнатуре типов вы сказали, что первые два аргумента имеют разные типы, но GHC определил, что (на основании вашего использования) они должны иметь одинаковый тип. Поэтому он жалуется, что не может определить, что weight
и height
имеют одинаковый тип, хотя должны (то есть предложенная вами сигнатура типов недостаточно строга и допускает некорректное использование функции).
weight
должен быть того же типа, что и height
, потому что вы их делите (никаких неявных приведений). weight ~ height
означает, что они одного типа. ghc немного объяснил, как он пришел к выводу, что weight ~ height
необходимо, извините. Вы можете сказать ему то, что он/вы хотели, используя синтаксис из расширения семейств типов:
{-# LANGUAGE TypeFamilies #-}
bmiTell :: (RealFloat weight, RealFloat height,weight~height) => weight -> height -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
Однако это тоже не идеально. Вы должны помнить, что в Haskell используется очень другая парадигма, и вы должны быть осторожны, чтобы не оказаться в ситуации, когда предполагается, что то, что было важно в другом языке, важно и здесь. Вы учитесь больше всего, когда вы находитесь вне своей зоны комфорта. Это как если бы кто-то из Лондона приехал в Торонто и пожаловался, что город непонятный, потому что все улицы одинаковые, в то время как кто-то из Торонто мог бы сказать, что Лондон непонятный, потому что на улицах нет регулярности. То, что вы называете запутыванием, хаскеллеры называют ясностью.
Если вы хотите вернуться к более объектно-ориентированной ясности цели, то сделайте bmiTell работающим только на людях, так что
data Person = Person {name :: String, weight :: Float, height :: Float}
bmiOffence :: Person -> String
bmiOffence p
| weight p / height p ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight p / height p ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight p / height p ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
Это, я полагаю, тот способ, которым вы бы сделали это ясным в ООП. Я действительно не верю, что вы используете тип аргументов вашего ООП-метода для получения этой информации, вы должны тайно использовать имена параметров для ясности, а не типы, и вряд ли справедливо ожидать, что haskell скажет вам имена параметров, когда вы исключили чтение имен параметров в своем вопросе.[см. * ниже] Система типов в Haskell удивительно гибкая и очень мощная, пожалуйста, не отказывайтесь от нее только потому, что она поначалу вызывает у вас отторжение.
Если вы действительно хотите, чтобы типы говорили вам, мы можем сделать это для вас:
type Weight = Float -- a type synonym - Float and Weight are exactly the same type, but human-readably different
type Height = Float
bmiClear :: Weight -> Height -> String
....
Такой подход используется для строк, представляющих имена файлов, поэтому мы определяем
type FilePath = String
writeFile :: FilePath -> String -> IO () -- take the path, the contents, and make an IO operation
что дает ясность, которую вы хотели получить. Однако считается, что
type FilePath = String
не хватает безопасности типов, и что
newtype FilePath = FilePath String
или что-то еще более умное было бы гораздо лучшей идеей. См. ответ Ben'а на очень важный вопрос о безопасности типов.
[*] Хорошо, вы можете сделать :t в ghci и получить сигнатуру типа без имени параметра, но ghci предназначен для интерактивной разработки исходного кода. Ваша библиотека или модуль не должны оставаться недокументированными и халтурными, вы должны использовать невероятно легкую синтаксическую систему документации haddock и установить haddock локально. Более законной версией вашей жалобы было бы то, что нет команды :v, которая печатает исходный код вашей функции bmiTell. Метрики показывают, что ваш код на Haskell для той же проблемы будет короче в несколько раз (в моем случае около 10 по сравнению с эквивалентным OO или не императивным кодом), поэтому показывать определение внутри gchi часто имеет смысл. Мы должны подать запрос на такую возможность.
Попробуйте это:
type Height a = a
type Weight a = a
bmiTell :: (RealFloat a) => Weight a -> Height a -> String
Возможно, не применимо к функции с piffling два аргумента, however... если у вас есть функция, которая принимает множество аргументов, аналогичных видах или просто непонятных заказов, может быть стоит определять структуру данных, которая представляет их. Например,
data Body a = Body {weight, height :: a}
bmiTell :: (RealFloat a) => Body a -> String
Теперь вы можете писать либо
bmiTell (Body {weight = 5, height = 2})
или
bmiTell (Body {height = 2, weight = 5})
и он стоит правильно в обе стороны, а также дамед очевидно для тех, кто пытается прочесть код.
Это's наверное больше стоит для функций с большим количеством аргументов, хотя. Всего два, я пошел бы со всеми и просто типа
его подписи документы, типа правильный порядок параметров, и вы получите ошибку компиляции, если вы смешиваете их.