Есть ли хороший способ сделать сигнатуры функций более информативными в Haskell?


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

Я начинаю изучать Haskell, в основном для моего собственного назидания, и мне нравится много идей и принципов, поддерживающих язык. Я увлекся функциональными языками после занятия по теории языка, где мы играли с Lisp, и я слышал много хорошего о том, насколько продуктивным может быть Хаскелл, поэтому я решил исследовать его сам. До сих пор мне нравится язык, за исключением одной вещи, от которой я не могу просто уйти: эти материнские сигнатуры функции.

мой профессиональный опыт в основном делает OO, особенно на Java. Большинство мест, где я работал, вбили много стандартных современных догм; гибкий, Чистый код, TDD и т. д. После нескольких лет работы таким образом, это определенно стало моим комфортом зона; особенно идея о том, что" хороший " код должен быть самодокументированным. Я привык работать в IDE, где длинные и подробные имена методов с очень описательными сигнатурами не являются проблемой с интеллектуальным автоматическим завершением и огромным массивом аналитических инструментов для навигации по пакетам и символам; если я могу нажать Ctrl+Space в Eclipse, то выведите, что делает метод, глядя на его имя и локальные переменные, связанные с его аргументами, вместо того, чтобы вытаскивать JavaDocs, Я счастлив как свинья в дерьме.

это, безусловно, не является частью лучших практик сообщества в Haskell. Я прочитал много разных мнений по этому вопросу, и я понимаю, что сообщество Haskell считает свою лаконичность "профессионалом". Я прошел через Как Читать Haskell, и я понимаю обоснование многих решений, но это не значит, что они мне нравятся; имена переменных с одной буквой и т. д. не для меня удовольствие. Я признаю, что Мне придется привыкнуть к этому, если я хочу продолжать взламывать язык.

но я не могу преодолеть сигнатуры функций. Возьмем этот пример, как вытащил из узнать вы на 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'

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

нет ли приемлемого способа создания сигнатур информативных функций? Является ли "путь Хаскелла"просто пикшей дерьмо из всего?

6 51

6 ответов:

подпись типа не является подписью в стиле Java. Подпись в стиле Java сообщит вам, какой параметр является весом, а какой-высотой только потому, что он смешивает имена параметров с типами параметров. Haskell не может сделать это как общее правило, потому что функции определяются с помощью сопоставления шаблонов и нескольких уравнений, как в:

map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []

здесь первый параметр называется f в первом уравнении и _ (что означает "безымянный") в второй. Второй параметр не есть имя в любом уравнении; в первых частях у него есть имена (и программист, вероятно, подумает об этом как о "списке xs"), а во втором это полностью буквальное выражение.

и тогда есть точечные определения, такие как:

concat :: [[a]] -> [a]
concat = foldr (++) []

подпись типа говорит нам, что он принимает параметр, который имеет тип [[a]], но имя для этого параметра не отображается в любом месте в система.

вне отдельного уравнения для функции имена, которые она использует для ссылки на свои аргументы, в любом случае не имеют значения за исключением в качестве документации. Поскольку идея "канонического имени" для параметра функции не очень хорошо определена в Haskell, место для информации " первый параметр bmiTell представляет вес, а второй представляет высоту" находится в документации, а не в подписи типа.

Я абсолютно согласен с тем, что функция does должна быть кристально чистой из" общедоступной " информации, доступной о ней. В Java это имя функции, а также типы и имена параметров. Если (как обычно) пользователю потребуется больше информации, вы добавляете ее в документацию. В Haskell общедоступной информацией о функции является имя функции и типы параметров. Если пользователю потребуется больше информации, вы добавите ее в документацию. Примечание IDEs для Haskell, такие как Leksah легко покажет вам Пикша комментирует.


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

  1. он принимает два параметра одного типа, представляющих разные вещи
  2. он будет делать неправильные вещи, если передаваемые параметры в неправильный порядок
  3. два типа не имеют естественного положения (как два [a] аргументы ++ do)

одна вещь, которая часто делается для повышения безопасности типа действительно сделать новые типы, как в ссылке, которую вы нашли. Я действительно не думаю, что это имеет много общего с передачей именованных параметров, более того, речь идет о создании типа данных, который явно представляет высота, а не любое другое количество, которое вы, возможно, захотите, чтобы измерить с рядом. Так Что Я не значения newtype появляются только при вызове; я бы использовал значение newtype везде, где я получил данные высоты С а также, и передавая его вокруг как данные о высоте, а не как число, так что я получаю тип-безопасность (и документация) выгоду везде. Я бы только развернул значение в сырое число, когда мне нужно передать его чему-то, что работает с числами, а не с высотой (например, арифметические операции внутри bmiTell).

обратите внимание, что это не имеет накладных расходов во время выполнения; newtypes представлены идентично данным "внутри" оболочки newtype, поэтому операции wrap/unwrap не являются операциями над базовым представлением и просто удаляются во время компиляции. Он добавляет только дополнительные символы в исходном коде, но эти символы ровно документация, которую вы ищете, с дополнительным преимуществом применения компилятором; подписи в стиле Java говорят вам, какие параметр-это вес и высота, но компилятор все равно не сможет сказать, если вы случайно передали их неправильно!

есть и другие варианты, в зависимости от того, как глупо и/или педантичный вы хотите сделать с вашими типами.

например,может сделать это...

type Meaning a b = a

bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String  
bmiTell weight height = -- etc.

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

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.

...но это все равно глупо и имеет тенденцию теряться, когда GHC расширяет синонимы типов.

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

то, что я бы рекомендовал вам попробовать, вместо этого, использует newtype обертки для указания семантики:

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)

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

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

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 использует очень действительно другая парадигма, и вы должны быть осторожны, чтобы не оказаться предполагая, что то, что было важно на другом языке важно здесь. Вы учитесь больше всего, когда находитесь вне своей зоны комфорта. Это похоже на то, что кто-то из Лондона появляется в Торонто и жалуется, что город сбивает с толку, потому что все улицы одинаковы, в то время как кто-то из Торонто может утверждать, что Лондон сбивает с толку, потому что на улицах нет регулярности. Что ты называешь запутывания называется ясности Haskellers.

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

если вы действительно хотите, типа, чтобы сказать вам, мы можем сделать это для вас:

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

или что-то еще умнее было бы намного лучше Идея. См. ответ Бена для очень важного пункта о безопасности типа.

[*] хорошо, вы можете сделать :t в ghci и получить подпись типа без имени параметра, но ghci предназначен для интерактивной разработки исходного кода. Ваша библиотека или модуль не должны оставаться недокументированными и взломанными, вы должны использовать невероятно легкий синтаксис пикши документирование системы и установка пикши локально. Более законной версией вашей жалобы было бы то, что нет команды a :v, которая печатает исходный код для вашей функции bmiTell. Метрики предполагают, что ваш код Haskell для той же проблемы будет короче в несколько раз (я нахожу около 10 в моем случае по сравнению с эквивалентным OO или не-OO императивным кодом), поэтому показ определения внутри gchi часто является разумным. Мы должны отправить запрос на функцию.

попробуйте это:

type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String

возможно, не имеет отношения к функции с двумя аргументами piffling,... Если у вас есть функция, которая принимает много и много аргументов, похожих типов или просто неясного порядка, возможно, стоит определить структуру данных, которая их представляет. Например,

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})

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

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