kzen.dev
  • Вопросы
  • Метки
  • Пользователи
Оповещения
Вознаграждения
Регистрация
После регистрации, сможете получать уведомления об ответах и комментариях на Ваши вопросы.
Вход
Если у Вас уже есть аккаунт, войдите чтобы проверить новые уведомления.
Тут будут вознаграждения за добавленные вопросы, ответы и комментарий.
Дополнительно
Источник
Редактировать
Doug Stephen
Doug Stephen
Вопрос

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

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

Я начинаю изучать 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, но это, кажется, немного перебор.

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

52 2012-09-14T00:42:04+00:00 6
Heinrich Apfelmus
Heinrich Apfelmus
Редактировал вопрос 14-го сентября 2012 в 1:52
Программирование
coding-style
functional-programming
syntax
haskell
Решение / Ответ
 Ben
Ben
14-го сентября 2012 в 1:49
2012-09-14T01:49:48+00:00
Дополнительно
Источник
Редактировать
#17204154

Подпись типа не на 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 немедленно отправляется предупреждающие знаки для меня, по следующим причинам:

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

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

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

80
0
C.  A. McCann
C. A. McCann
14-го сентября 2012 в 1:42
2012-09-14T01:42:26+00:00
Дополнительно
Источник
Редактировать
#17204153

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

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

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

37
0
 singpolyma
singpolyma
14-го сентября 2012 в 12:50
2012-09-14T00:50:25+00:00
Дополнительно
Источник
Редактировать
#17204150

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

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

 leftaroundabout
leftaroundabout
Редактировал ответ 14-го сентября 2012 в 6:11
27
0
 AndrewC
AndrewC
14-го сентября 2012 в 1:41
2012-09-14T01:41:46+00:00
Дополнительно
Источник
Редактировать
#17204152

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 часто имеет смысл. Мы должны подать запрос на такую возможность.

 AndrewC
AndrewC
Редактировал ответ 14-го сентября 2012 в 4:02
14
0
Gabriel Gonzalez
Gabriel Gonzalez
14-го сентября 2012 в 1:41
2012-09-14T01:41:13+00:00
Дополнительно
Источник
Редактировать
#17204151

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

type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String
13
0
 MathematicalOrchid
MathematicalOrchid
14-го сентября 2012 в 3:41
2012-09-14T15:41:30+00:00
Дополнительно
Источник
Редактировать
#17204155

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

 hammar
hammar
Редактировал ответ 14-го сентября 2012 в 3:44
12
0
Похожие сообщества 4
Haskell
Haskell
1 665 пользователей
https://combot.org/chat/-1001043143583 Ссылки на полезные ресурсы: https://ruhaskell.org/links.html ; Информация о мероприятиях: https://gist.github.com/qnikst/a96cac661be80d126d0829f2ced1916e
Открыть telegram
Haskell Start
Haskell Start
895 пользователей
Чат для вопросов по основам хаскеля для совсем начинающих. Правила чата: http://bit.ly/λrules Остальные чаты перечислены тут: https://ruhaskell.org/links Wiki сообщества: http://bit.ly/λwiki (про редакторы там тоже написано)
Открыть telegram
Haskell CVs and Jobs
Haskell CVs and Jobs
537 пользователей
RuHaskell.org Discuss Haskell employers, vacancies and job market. Show your CV! Job postings are in @haskell_job General discussion in @haskellru, @haskell_learn, @haskell_en Offtop: @haskell_cv_blah Code of Conduct: http://bit.ly/λrules
Открыть telegram
ФП
ФП
230 пользователей
Все о функциональных языках
Открыть telegram
Добавить вопрос
Категории
Все
Технологий
Культура / Отдых
Жизнь / Искусство
Наука
Профессии
Бизнес
Пользователи
Все
Новые
Популярные
1
Ilya Smirnov
Зарегистрирован 1 день назад
2
Денис Васьков
Зарегистрирован 2 дня назад
3
Dima Patrushev
Зарегистрирован 4 дня назад
4
sirojidddin otaboyev
Зарегистрирован 1 неделю назад
5
Елена Гайдамамакинат
Зарегистрирован 1 неделю назад
ID
KO
RU
© kzen.dev 2023
Источник
stackoverflow.com
под лицензией cc by-sa 3.0 с атрибуцией