Подтвердить что ты не робот

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

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

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

Мой профессиональный фон в основном делает OO, особенно на Java. Большинство мест, в которых я работал, забили много стандартных современных догм; Agile, Clean Code, 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'

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

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

4b9b3361

Ответ 1

Подпись типа не является сигнатурой в стиле 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 представляет вес, а второй представляет высоту" - в документации, а не в типе подпись.

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


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

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

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

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

Ответ 2

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

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

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)

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

Ответ 3

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

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

Ответ 4

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 сообщит вам имена параметров когда вы исключили чтение имен параметров в своем вопросе. [см. * ниже] Система типов в 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 предназначен для интерактивной разработки исходного кода. Ваша библиотека или модуль не должны оставаться недокументированными и взломанными, вы должны использовать невероятно легкую синтаксическую систему документации для буферизации и установить локальную локальную сеть. Более законной версией вашей жалобы было бы то, что не существует команды: v, которая печатает исходный код для вашей функции bmiTell. Метрики предполагают, что ваш код Haskell для той же проблемы будет короче по многу (я нахожу около 10 в моем случае по сравнению с эквивалентным ОО или несовместимым императивным кодом), поэтому определение определения внутри gchi часто разумно. Мы должны предоставить запрос функции.

Ответ 5

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

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

Ответ 6

Попробуйте следующее:

type Height a = a
type Weight a = a

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