语法-在Haskell中是否有一种使函数签名更具信息性的好方法?

我意识到这可能被认为是一个主观的问题,或者是一个离题的问题,因此我希望与其关闭它,不如将其移植到程序员。

我开始学习Haskell,主要是为了自己的学习,并且我喜欢支持该语言的许多思想和原则。 在参加与Lisp一起进行的语言理论课程后,我对函数式语言着迷,并且我听到了很多关于Haskell可以如何高效工作的好消息,因此我想自己进行调查。 到目前为止,除了一种我无法摆脱的事情之外,我喜欢这种语言:那些母亲会发出函数签名。

我的专业背景主要是面向对象的,尤其是在Java中。 我工作过的大多数地方都采用了许多标准的现代教条。 经过几年的这种工作方式,它绝对成为我的舒适之地。 特别是“好的”代码应该自我记录的想法。 我已经习惯了在IDE中工作,在IDE中,带有描述性很强的冗长而冗长的方法名称不是问题,它具有智能自动完成功能以及用于导航包和符号的大量分析工具。 如果我可以在Eclipse中按Ctrl + Space,然后通过查看方法的名称以及与其参数相关联的局部作用域变量(而不是拉出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!"

我意识到这是一个愚蠢的示例,仅出于解释防护和类约束的目的而创建,但是如果您仅检查该函数的签名,则不会知道它的哪个参数是权重。 或高度。 即使您要使用newtypeDouble而不是任何类型,也仍然无法立即分辨出来。

一开始,我以为我会很可爱,聪明又聪明,并尝试使用带有多个类约束的更长的类型变量名来欺骗它:

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'

我不完全了解为什么这行不通,所以我开始在Google上搜索,甚至发现了这篇小文章,其中建议了命名参数,特别是通过newtype欺骗了命名参数,但这似乎有点多。

有没有可以接受的方法来制作信息功能签名? 是“ The Haskell Way”仅仅是为了让Haddock摆脱一切吗?

6个解决方案
79 votes

类型签名不是Java样式的签名。 Java样式的签名只会告诉您哪个参数是重量,哪个参数是高度,这是因为它将参数名称与参数类型混合在一起。 Haskell不能这样做,因为函数是使用模式匹配和多个方程式定义的,如下所示:

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

在这里,第一个参数在第一个方程式中命名为f,在第二个方程式中命名为bmiTell(几乎意味着“未命名”)。 第二个参数在两个方程式中都没有名称; 在它的第一部分中有名称(程序员可能会将其视为“ xs列表”),而在第二部分中则是完全文字表达。

还有一些无意义的定义,例如:

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

类型签名告诉我们它采用了bmiTell类型的参数,但是该参数的名称在系统中的任何位置都没有出现。

在函数的单个方程式之外,用于指称其参数的名称与文档无关。 由于在Haskell中没有很好地定义函数参数的“规范名称”的概念,因此“文档bmiTell的第一个参数表示重量,而第二个参数表示高度”信息的位置在文档中,而不是在类型签名中。

我完全同意,函数的功能应该从有关该函数的“公共”信息中清晰可见。 在Java中,这是函数的名称以及参数类型和名称。 如果(通常)用户需要更多信息,则可以在文档中添加它。 在Haskell中,有关函数的公共信息是函数的名称和参数类型。 如果用户将需要更多信息,请在文档中添加。 注意Haskell的IDE(例如Leksah)将轻松向您显示Haddock注释。


请注意,使用像Haskell那样的具有强大表达能力的类型系统的语言进行的首选操作通常是尝试使尽可能多的可检测错误与类型错误一样。 因此,由于以下原因,像bmiTell这样的函数会立即向我发出警告信号:

  1. 它采用两个相同类型的参数表示不同的事物
  2. 如果以错误的顺序传递参数,它将做错事情
  3. 这两种类型没有自然位置(就像++353的两个bmiTell参数一样)

实际上,增加类型安全性通常要做的一件事就是创建新类型,如您所找到的链接。 我真的不认为这与命名参数传递有太大关系,更多的是要创建一个明确表示高度的数据类型,而不是您可能想用数字测量的任何其他数量。 因此,我不会让newtype值仅出现在调用中。 无论从哪里获取高度数据,我都将使用newtype值,并将其作为高度数据而不是数字传递,这样我就可以在任何地方获得类型安全性(和文档)。 只有在需要将值传递给对数字而不是对高度进行运算的东西(例如bmiTell内的算术运算)时,才将其解包装为原始数。

注意,这没有运行时开销。 新类型的表示与“新类型”包装器中的“数据”相同,因此,包装/展开操作在基础表示形式上是无操作的,在编译过程中可以将其删除。 它仅在源代码中添加了额外的字符,但是这些字符正是您要查找的文档,具有由编译器强制执行的附加好处; Java风格的签名告诉您哪个参数是weight和哪个是height,但是编译器仍然无法判断您是否不小心以错误的方式传递了它们!

Ben answered 2020-01-23T07:35:18Z
37 votes

还有其他选项,具体取决于您希望使用哪种类型的愚蠢和/或学究的人。

例如,您可以执行此操作...

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扩展类型同义词时,它往往会迷路。

真正的问题是,您要将附加的语义内容附加到相同多态类型的不同值上,这违背了语言本身的本质,因此通常不是惯用语言。

当然,一种选择是只处理非信息类型变量。 但是,如果在相同类型的两个事物之间的区别并不明显(从给出的顺序来看),这并不能令人满意。

我建议您尝试使用GeneralizedNewtypeDeriving包装器指定语义:

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扩展,即使对于通常无法派生的类型类,您甚至可以获得自动实例。

C. A. McCann answered 2020-01-23T07:36:16Z
27 votes

Haddocks和/或还查看函数方程(将事物绑定到的名称)是我告诉发生了什么的方式。 您可以像这样添加Haddock的各个参数,

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已确定(根据您的使用)它们必须具有相同的类型。 因此,它抱怨无法确定weightheight是相同的类型,即使它们必须是相同的类型(也就是说,建议的类型签名不够严格,并且会允许对该函数的无效使用)。

singpolyma answered 2020-01-23T07:36:59Z
14 votes

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!"

我认为,这是在OOP中明确说明的一种方式。 我真的不相信您使用的是OOP方法参数的类型来获取此信息,为了清晰起见,您必须秘密使用参数名称,而不是使用类型,并且期望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的答案。

[*]好的,您可以在ghci中执行:t并获得不带参数名称的类型签名,但是ghci用于交互式开发源代码。 您的库或模块不应保持无文档记录和骇人听闻,应使用轻量级的语法haddock文档系统并在本地安装haddock。 您抱怨的一个更合理的版本是,没有:v命令可以显示函数bmiTell的源代码。 度量标准表明,针对同一问题的Haskell代码将缩短一个因数(在我的案例中,与等效的OO或非oo命令性代码相比,我发现它大约有10个),因此显示gchi内的定义通常是明智的。 我们应该提交功能请求。

AndrewC answered 2020-01-23T07:38:03Z
12 votes

尝试这个:

type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String
Gabriel Gonzalez answered 2020-01-23T07:38:23Z
12 votes

但是,可能与带有两个参数的函数无关。...如果您有一个函数,需要使用大量参数,相似类型或只是顺序不明确的参数,则可能值得定义一个表示它们的数据结构。 例如,

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,以便类型签名记录正确的参数顺序,如果将它们混在一起,则会出现编译时错误。

MathematicalOrchid answered 2020-01-23T07:39:02Z
translate from https://stackoverflow.com:/questions/12416723/is-there-a-nice-way-to-make-function-signatures-more-informative-in-haskell