56

これは主観的な質問またはトピックから外れた質問と見なされる可能性があることを認識しているので、閉じるのではなく、プログラマーに移行されることを願っています。

私は主に自分自身の啓蒙のためにHaskellを学び始めており、言語を裏付ける多くのアイデアや原則が好きです。Lispで遊んだ言語理論のクラスを受講した後、関数型言語に魅了されました。Haskellの生産性について多くの良いことを聞いていたので、自分で調べてみようと思いました。これまでのところ、私はこの言語が好きですが、私がただ逃げることができない1つのことを除いて、それらの母親は関数の署名を無効にします。

私の専門的な経歴は、特にJavaで主にOOを行っています。私が働いてきた場所のほとんどは、多くの標準的な現代の教義に打ちのめされてきました。アジャイル、クリーンコード、TDDなど。このように数年間作業した後、それは間違いなく私の快適ゾーンになりました。特に、「優れた」コードは自己文書化する必要があるという考えです。私はIDEでの作業に慣れてきました。ここでは、非常にわかりやすい署名を持つ長くて冗長なメソッド名は、インテリジェントなオートコンプリートとパッケージやシンボルをナビゲートするための膨大な数の分析ツールでは問題になりません。EclipseでCtrl+Spaceを押して、JavaDocsをプルアップする代わりに、メソッドの名前と引数に関連付けられたローカルスコープの変数を調べて、メソッドが何をしているのかを推測できれば、うんちをしている豚のように幸せです。

これは明らかに、Haskellのコミュニティのベストプラクティスの一部ではありません。私はこの問題について多くの異なる意見を読みましたが、Haskellコミュニティはその簡潔さを「プロ」と見なしていることを理解しています。私はHaskellの読み方を読み、多くの決定の背後にある理論的根拠を理解していますが、それは私がそれらを好きだという意味ではありません。一文字の変数名などは私には面白くありません。私はその言語でハッキングを続けたいのであれば、それに慣れる必要があることを認めます。

しかし、関数のシグネチャを乗り越えることはできません。Learn you aHaskell[...]の関数構文に関するセクションから抜粋した次の例を見てください。

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することさえ見つけましたが、それは少し多いようです。

有益な関数シグネチャを作成するための許容できる方法はありませんか?「TheHaskellWay」は、単にすべてのがらくたをハドックするためのものですか?

4

6 に答える 6

82

型シグニチャはJavaスタイルのシグニチャではありません。Javaスタイルのシグニチャは、パラメータ名とパラメータタイプが混在しているため、どのパラメータが重みで、どのパラメータが高さであるかを示します。Haskellはこれを原則として行うことはできません。これは、関数が次のようにパターンマッチングと複数の方程式を使用して定義されているためです。

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

ここで、最初のパラメーターはf最初の式で名前が付けられ、 _(ほとんどの場合「名前なし」を意味します)2番目の式で名前が付けられます。2番目のパラメーターには、どちらの式にも名前ありません。最初の部分には名前があり(プログラマーはおそらく「xsリスト」と考えるでしょう)、2番目の部分には完全にリテラルの式です。

そして、次のようなポイントフリーの定義があります。

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

型シグネチャは、型のパラメータを受け取ることを示していますが[[a]]、このパラメータの名前はシステムのどこにも表示されません。

関数の個々の方程式の外では、引数を参照するために使用する名前は、ドキュメントとしての場合を除いて、とにかく無関係です。関数のパラメーターの「正規名」の概念はHaskellで十分に定義されていないため、「最初のパラメーターはbmiTell重量を表し、2番目のパラメーターは高さを表す」という情報の場所は、型署名ではなくドキュメントにあります。

私は、関数が何をするかは、それについて入手可能な「公開」情報から非常に明確でなければならないことに完全に同意します。Javaでは、これが関数の名前であり、パラメーターのタイプと名前です。(一般的に)ユーザーがそれよりも多くの情報を必要とする場合は、ドキュメントに追加します。Haskellでは、関数に関する公開情報は関数の名前とパラメーターの種類です。ユーザーがそれ以上の情報を必要とする場合は、ドキュメントに追加します。LeksahなどのHaskell用のIDEは、Haddockのコメントを簡単に表示します。


Haskellのような強力で表現力豊かな型システムを備えた言語で行うのが好ましいことは、多くの場合、型エラーと同じくらい多くのエラーを検出できるようにすることです。したがって、bmiTell次のような理由で、次のような関数がすぐに警告サインを発します。

  1. 異なるものを表す同じタイプの2つのパラメーターを取ります
  2. パラメータを間違った順序で渡すと、間違った処理を実行します
  3. 2つのタイプには自然な位置がありません(2つの[a]引数が++行うように)

型の安全性を高めるためによく行われることの1つは、実際に、見つけたリンクのように、新しい型を作成することです。これは、名前付きパラメーターの受け渡しとはあまり関係がないとは思いません。数値で測定したい他の量ではなく、高さを明示的に表すデータ型を作成することです。したがって、呼び出し時にのみnewtype値が表示されることはありません。高さデータを取得した場所では常にnewtype値を使用します同様に、数値ではなく高さデータとして渡すことで、あらゆる場所で型安全性(およびドキュメント)のメリットを得ることができます。高さではなく数値を操作するもの(内部の算術演算など)に値を渡す必要がある場合にのみ、値を生の数値にアンラップしますbmiTell

これには実行時のオーバーヘッドがないことに注意してください。newtypeは、newtypeラッパーの「内部」のデータと同じように表されるため、ラップ/アンラップ操作は、基になる表現に対する操作なしであり、コンパイル中に削除されるだけです。ソースコードに追加の文字のみが追加されますが、これらの文字はまさにあなたが探しているドキュメントであり、コンパイラによって強制されるという追加の利点があります。Javaスタイルのシグニチャは、どのパラメータが重みでどれが高さであるかを示しますが、コンパイラは、誤ってそれらを間違った方法で渡したかどうかを判断できません。

于 2012-09-14T01:49:48.573 に答える
37

あなたがあなたのタイプでどれだけ愚かで、そして/または衒学者になりたいかに応じて、他のオプションがあります。

たとえば、これを行うことができます...

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が型の同義語を拡張すると失われる傾向があります。

ここでの本当の問題は、同じポリモーフィックタイプの異なる値に追加のセマンティックコンテンツを添付していることです。これは、言語自体の粒度に反するため、通常は慣用的ではありません。

もちろん、1つのオプションは、情報量の少ない型変数を処理することです。しかし、同じタイプの2つのものの間に、与えられた順序から明らかではない重要な違いがある場合、それはあまり満足のいくものではありません。

代わりに、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すると、通常は派生できない型クラスの場合でも、自動インスタンスを取得することもできます。

于 2012-09-14T01:42:26.843 に答える
27

ハドックおよび/または関数方程式(バインドした名前)を調べることは、私が何が起こっているかを伝える方法です。あなたはそのように、個々のパラメータをハドックすることができます、

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

したがって、この型シグネチャでは、最初の2つの引数の型が異なると述べましたが、GHCは、(使用に基づいて)同じ型でなければならないと判断しました。weightそのため、同じ型である必要があるにもかかわらず、同じ型であると判断できないと文句を言いheightます(つまり、提案された型シグネチャは十分に厳密ではなく、関数の無効な使用を許可します)。

于 2012-09-14T00:50:25.840 に答える
14

weightそれらを分割しているためと同じタイプでheightある必要があります(暗黙のキャストはありません)。weight ~ height同じタイプであることを意味します。weight ~ heightghcは、必要な結論に至った経緯を少し説明しました。申し訳ありません。型族拡張機能の構文を使用して、何を使用したいかを伝えることができます。

{-# 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

または、さらにスマートなものがはるかに優れたアイデアになります。型安全性に関する非常に重要な点については、ベンの回答を参照してください。

[*] OK、ghciで:tを実行して、パラメーター名なしで型シグネチャを取得できますが、ghciはソースコードのインタラクティブな開発用です。ライブラリまたはモジュールが文書化されておらず、ハッキーなままであってはなりません。信じられないほど軽量な構文のハドックドキュメンテーションシステムを使用して、ハドックをローカルにインストールする必要があります。苦情のより正当なバージョンは、関数bmiTellのソースコードを出力する:vコマンドがないことです。メトリックは、同じ問題のHaskellコードが1倍短くなることを示唆しています(私の場合、同等のOOまたは非OO命令型コードと比較して約10が見つかります)。したがって、gchi内の定義を示すことはしばしば賢明です。機能リクエストを送信する必要があります。

于 2012-09-14T01:41:46.480 に答える
13

これを試して:

type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String
于 2012-09-14T01:41:13.430 に答える
12

ただし、2つの引数を使用する関数とは関係がない可能性があります。ただし、多くの引数をとる関数、類似したタイプ、または順序が不明確な関数がある場合は、それらを表すデータ構造を定義する価値があります。例えば、

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

そして、それは両方の方法で正しく価値があり、あなたのコードを読もうとしている人には明らかなことです。

ただし、引数の数が多い関数の方がおそらく価値があります。たった2つだけ、私は他のみんなと一緒に行きますnewtype。型署名が正しいパラメーターの順序を文書化し、それらを混同するとコンパイル時エラーが発生するようにします。

于 2012-09-14T15:41:30.733 に答える