私はLearn You a Haskellを読んでいますが、モナドの章では、()
があらゆるタイプの一種の「null」として扱われているようです。 GHCiで()
のタイプを確認すると、
>> :t ()
() :: ()
これは非常に紛らわしい文です。 ()
はそれ自体が型であるようです。それがどのように言語に適合するか、どのようにどんなタイプにも耐えられるように見えるかについて私は混乱しています。
tl; dr _()
_は、「null」値をすべてのタイプに追加するわけではありません。 _()
_は、それ自体の型の「ダル」値です:_()
_。
質問から少し離れて、一般的な混乱の原因に対処しましょう。 Haskellを学習するときに吸収する重要なことは、そのexpression言語とそのtype言語。おそらく、2つが別々に保たれていることをご存知でしょう。しかし、それは同じシンボルが両方で使用されることを可能にし、それはここで起こっていることです。あなたが見ている言語を伝える簡単なテキストの合図があります。これらのキューを検出するために言語全体を解析する必要はありません。
Haskellモジュールの最上位レベルは、デフォルトでは式言語にあります。式の間に方程式を記述することにより、関数を定義します。しかし、式言語でfoo::barが表示されると、は、fooが式であり、barがそのタイプであることを意味します。したがって、_() :: ()
_を読むと、式言語の_()
_を型言語の_()
_と関連付けるステートメントが表示されます。 2つの_()
_シンボルは、同じ言語ではないため、異なる意味を持ちます。表現/型言語の分離が潜在意識にそれ自体をインストールするまで、この繰り返しはしばしば初心者を混乱させます。
キーワードdata
は、最初に新しい型が何であるか、次にその値が何であるかを示すように、式と型言語の慎重な混合を含む新しいデータ型宣言を導入します。
データ TyCon tyvar ... tyvar = ValCon1タイプ...タイプ | ... | ValConnタイプ...タイプ
このような宣言では、型コンストラクターTyConが型言語に追加されており、ValCon値コンストラクターが式言語(およびそのパターンのサブ言語)に追加されています。 data
宣言では、ValConsの引数の位置にあるものは、そのときに引数に与えられた型を示しますValConは式で使用されます。例えば、
_data Tree a = Leaf | Node (Tree a) a (Tree a)
_
ノードに要素を格納するバイナリツリータイプのタイプコンストラクターTree
を宣言します。ノードの値は、値コンストラクターLeaf
およびNode
によって指定されます。タイプコンストラクター(ツリー)を青、値コンストラクター(リーフ、ノード)を赤に色付けするのが好きです。式には青色がなく、(高度な機能を使用している場合を除き)型には赤色がありません。組み込み型Bool
を宣言できますが、
_data Bool = True | False
_
青Bool
を型言語に追加し、赤True
およびFalse
を式言語に追加します。悲しいことに、私のmarkdown-fuはこの投稿に色を追加するタスクには不十分です。そのため、頭に色を追加することを学ぶ必要があります。
「ユニット」タイプは_()
_を特別なシンボルとして使用しますが、宣言されているように機能します
_data () = () -- the left () is blue; the right () is red
_
つまり、概念的に青い_()
_は型言語の型コンストラクターですが、概念的に赤い_()
_は式言語の値コンストラクターであり、実際には_() :: ()
_です。 [このようなしゃれの唯一の例ではありません。大きなタプルのタイプは同じパターンに従います。ペア構文は次のようになります
_data (a, b) = (a, b)
_
型言語と式言語の両方に(、)を追加します。しかし、私は脱線します。
したがって、タイプ「ユニット」と発音されることが多いタイプ_()
_は、話す価値のある値を1つ含むタイプです。その値は_()
_で記述されますが、式言語で記述され、「ボイド」と発音されることもあります。値が1つだけの型はあまり面白くありません。タイプ_()
_の値は、情報のゼロビットを提供します。それが何であるかを既に知っています。したがって、副作用を示すタイプ_()
_について特別なものはありませんが、多くの場合、モナド型の値コンポーネントとして表示されます。モナド演算は、次のようなタイプを持つ傾向があります
val-in-type-1 -> ...-> val-in-type-n -> 効果モナドval-out-type
ここで、戻り値の型は型アプリケーションです。関数はどの効果が可能かを示し、引数は操作によってどのような値が生成されるかを示します。例えば
_put :: s -> State s ()
_
(アプリケーションは左側に関連付けられているため(「60年代に私たち全員が行ったように」、ロジャーヒンドリー))
_put :: s -> (State s) ()
_
1つの値入力タイプs
、エフェクトモナド_State s
_、および値出力タイプ_()
_があります。 _()
_が値の出力タイプとして表示される場合、それは単に「この操作はそのeffectにのみ使用されることを意味します;配信される値は興味深いものではありません「。同様に
_putStr :: String -> IO ()
_
文字列をstdout
に配信しますが、エキサイティングなものは何も返しません。
_()
_型は、データがshapeのみで構成されていることを示すコンテナのような構造の要素型としても役立ちます。興味深いペイロードなし。たとえば、Tree
が上記のように宣言されている場合、Tree ()
はバイナリツリー形状のタイプであり、ノードに関心のあるものは何も格納しません。同様に、[()]
は鈍い要素のリストのタイプであり、リストの要素に関心のあるものがない場合、貢献する情報はその長さだけです。
まとめると、_()
_は型です。その1つの値_()
_は、たまたま同じ名前を持っていますが、型言語と式言語が分離されているので大丈夫です。コンテキスト(たとえば、モナドまたはコンテナ)で、コンテキストのみが興味深いことを伝えるため、「情報なし」を表す型があると便利です。
_()
_タイプは、ゼロ要素のタプルと考えることができます。値は1つしか持てないタイプであるため、タイプが必要な場合に使用されますが、実際に情報を伝える必要はありません。これのいくつかの使用法があります。
IO
やState
などのモナド的なものには、戻り値があり、副作用を実行します。操作の唯一のポイントは、画面への書き込みや状態の保存などの副作用を実行することだけです。画面に書き込むには、putStrLn
の型が_String -> IO ?
_である必要があります-IO
は常に何らかの戻り値の型を持っている必要がありますが、ここには返すのに役立つものはありません。それでは、どの型を返すべきですか? Intと言うことができ、常に0を返しますが、それは誤解を招きます。したがって、値が1つしかない(したがって有用な情報がない)型である_()
_を返し、戻ってくる有用なものがないことを示します。
有用な値を持たない型を持つことが便利な場合があります。タイプk
のキーをタイプv
の値にマッピングするタイプ_Map k v
_を実装したかどうかを検討してください。次に、Set
を実装します。これは、値部分がなく、キーだけが必要なことを除いて、マップに実際に似ています。 Javaのような言語では、ダミー値の型としてブール値を使用できますが、実際には、有用な値を持たない型が必要です。したがって、type Set k = Map k ()
_()
_は特に魔法ではないことに注意してください。必要に応じて、変数に保存し、パターンマッチングを実行できます(あまり意味はありませんが)。
_main = do
x <- putStrLn "Hello"
case x of
() -> putStrLn "The only value..."
_
タプルとの類推で()
を考えるのが本当に好きです。
(Int, Char)
は、Int
とChar
のすべてのペアのタイプであるため、その値は、Int
のすべての可能な値と交差するChar
のすべての可能な値です。 (Int, Char, String)
は、同様に、Int
、Char
、およびString
のすべてのトリプルのタイプです。
このパターンを上方向に拡張し続ける方法は簡単にわかりますが、下方向はどうでしょうか?
(Int)
は、「1-Tuple」タイプで、Int
のすべての可能な値で構成されます。しかし、それはHaskellによってInt
を単に括弧で囲み、したがってInt
型だけであると解析されます。また、このタイプの値は(1)
、(2)
、(3)
などになります。これらも、括弧内の通常のInt
値として解析されます。しかし、考えてみると、「1-Tuple」は単一の値とまったく同じであるため、実際にそれらを存在させる必要はありません。
ゼロタプルまでさらに1ステップ下に進むと、()
が得られます。これは、空のタイプのリストにある値のすべての可能な組み合わせである必要があります。まあ、それを行うには1つの方法があります。それは他の値を含まないことです。したがって、タイプ()
には値が1つしかありません。そして、タプル値の構文との類推によって、その値を()
として書くことができます。これは確かにlooks値のないタプルのようです.
それがまさにその仕組みです。魔法はありません。このタイプ()
とその値()
は、言語によって特別に扱われることはありません。
()
は、実際にはLYAHブックのモナドの例で「すべての型のnull値」として扱われていません。タイプ()
が使用されるときは常に、返されるonly値は()
です。そのため、explicitlyの型として使用されますすることはできませんその他の戻り値。同様に、別の型が返されることになっている場合、cannot return ()
。
覚えておくべきことは、多くのモナド計算がdo
ブロックまたは>>=
、>>
などの演算子と一緒に構成されている場合、いくつかのモナドm
に対してm a
型の値を構築することです。 m
の選択は、コンポーネントパーツ全体で同じままである必要があります(そのようにMaybe Int
をIO Int
で構成する方法はありません)が、a
は各段階で異なる場合があります。
したがって、誰かがIO String
計算の途中でIO ()
を貼り付けた場合、String
型で()
をnullとして使用していないため、単にsing an IO ()
IO String
を構築する途中で、同じ方法でseInt
を構築する途中でString
を構築します。
混乱は他のプログラミング言語から生じます。「void」は、ほとんどの命令型言語では、値を格納するメモリに構造がないことを意味します。 「boolean」には2ビットではなく2つの値があり、「void」には値ではなくビットがないため、一貫性がないように見えますが、約what関数は実用的な意味で戻ります。正確には、その単一の値はストレージを少しも消費しません。
値bottom(_|_
と書かれている)をしばらく無視しましょう...
()
はUnitと呼ばれ、null-Tupleのように書かれています。値は1つだけです。そして、 Void
には値さえないので、Void
と呼ばれません。したがって、どの関数からも返されません。
これに注意してください:Bool
には2つの値(True
およびFalse
)、()
には1つの値(()
)、および Void
には値がありません(存在しません)。それらは、要素が2つ/ 1つないセットのようなものです。値を保存するのに必要な最小メモリは、それぞれ1ビット/ビットなし/不可能です。つまり、()
を返す関数は、役に立たない可能性のある結果値(明らかな値)を返します。一方、Void
は、結果が存在しないため、その関数が返されず、結果が表示されないことを意味します。
「その値」に名前を付けたい場合、関数が返す値を返すことはありません(はい、これはcrazytalkのように聞こえます)。それをbottom(「_|_
」、逆Tのように記述)と呼びます。例外、無限ループ、デッドロック、または「長く待つ」ことを表します。 (一部の関数は、パラメーターの1つがbottomである場合にのみbottomを返します。)
デカルト積/これらのタイプのタプルを作成すると、同じ動作が観察されます。(Bool,Bool,Bool,(),())
には2・2・2・1・1 = 6の異なる値があります。 (Bool,Bool,Bool,(),Void)
は、{t、f}×{t、f}×{t、f}×{u}×{}のセットに似ています。ただし、要素は2・2・2・1・0 = 0です。ただし、 _|_
を値としてカウントします。
さらに別の角度:
()
は、()
という単一の要素を含むセットの名前です。
確かに、この場合、セットの名前とその中の要素が同じであることが少しわかりにくいです。
覚えておいてください:Haskellでは、型はその中の要素として可能な値を持つ集合です。