Learn You a Haskellを読んでいますが、モナドの章では()
、すべてのタイプに対して一種の「null」として扱われているようです。()
GHCi での型を確認すると、
>> :t ()
() :: ()
これは非常に紛らわしい声明です。それは()
それ自体のタイプのようです。それが言語にどのように適合するのか、どのようにどのタイプを表すことができるように見えるのかについて、私は混乱しています。
この()
型は、ゼロ要素のタプルと考えることができます。これは 1 つの値しか持てない型であるため、型を持つ必要がある場合に使用されますが、実際には情報を伝える必要はありません。これにはいくつかの用途があります。
モナドのようなものは戻り値IO
をState
持ち、副作用を実行します。操作の唯一のポイントが、画面への書き込みや状態の保存などの副作用を実行することである場合があります。画面に書き込むにputStrLn
は型が必要ですString -> IO ?
--IO
常に戻り値の型が必要ですが、ここでは返すのに役立つものは何もありません。では、どの型を返す必要があるのでしょうか。Int と言って常に 0 を返すこともできますが、それは誤解を招きます。そのため、値が 1 つしかない (つまり有用な情報がない) 型を返し()
、有用なものが返されないことを示します。
有用な値を持たない型を持つと便利な場合があります。type のキーを typeの値にMap k v
マップする型を実装したかどうかを検討してください。次に、 を実装します。これは、値の部分が必要なく、キーだけが必要であることを除いて、マップに非常に似ています。Java のような言語では、ブール値をダミーの値の型として使用できますが、実際には、有用な値を持たない型が必要なだけです。だからあなたは言うことができますk
v
Set
type Set k = Map k ()
()
特に魔法ではないことに注意してください。必要に応じて、変数に格納してパターン マッチを実行できます (あまり意味はありませんが)。
main = do
x <- putStrLn "Hello"
case x of
() -> putStrLn "The only value..."
()
タプルとの類推で考えるのが本当に好きです。
(Int, Char)
Int
は anと aのすべてのペアの型であるChar
ため、その値は のすべての可能な値と のすべての可能な値をInt
交差させたものですChar
。同様に、 an 、 a 、および a(Int, Char, String)
のすべてのトリプルの型です。Int
Char
String
このパターンを上向きに拡張し続ける方法は簡単にわかりますが、下向きはどうでしょうか?
(Int)
のすべての可能な値で構成される「1タプル」タイプになりますInt
。しかし、これは Haskell によって単に括弧で囲まれたものとして解析されるInt
ため、単なる型になりInt
ます。この型の値は(1)
、(2)
、(3)
などで、これもInt
括弧内の通常の値として解析されます。しかし、考えてみれば、「1-タプル」は単一の値とまったく同じなので、実際に存在する必要はありません。
ゼロタプルにさらに一歩下がると、 が得られます()
。これは、型の空のリスト内の値のすべての可能な組み合わせである必要があります。それを行う方法は 1 つだけです。それは、他の値を含まないこと()
です。そして、タプル値の構文との類推により、その値を のように書くことができます。()
これは確かに、値を含まないタプルのように見えます。
それがまさにその仕組みです。魔法はなく、この型()
とその値()
が言語によって特別に扱われることは決してありません。
()
LYAH book のモナドの例では、実際には「任意の型の null 値」として扱われていません。()
タイプが使用されるときはいつでも、返される唯一の値は です()
。したがって、他の戻り値はあり得ないことを明示的に示す型として使用されます。同様に、別の型が返されるはずの場所では、 を返すことはできません。()
心に留めておくべきことは、一連のモナド計算が 、 などのブロックまたは演算子と一緒に構成される場合do
、それらはいくつかの モナドの型の値を構築することになるということです。のその選択は、コンポーネント パーツ全体で同じままでなければなりません (そのように を で構成する方法はありません)が、缶と非常に多くの場合、各段階で異なります。>>=
>>
m a
m
m
Maybe Int
IO Int
a
したがって、誰かが計算IO ()
の途中で an を突き刺した場合、それは を型の null として使用するのではなく、 を構築する途中でanを使用するのと同じ方法で、を構築する途中でan を使用するだけです。IO String
()
String
IO ()
IO String
Int
String
混乱は他のプログラミング言語から来ています。「無効」とは、ほとんどの命令型言語では、値を格納する構造がメモリにないことを意味します。「boolean」には2ビットではなく2つの値があり、「void」には値がないのではなくビットがないため、矛盾しているように見えますが、実用的な意味で関数が返すものについてです。正確に言うと、その単一の値はストレージをまったく消費しません。
値の底(書かれている)をしばらく無視しましょう_|_
...
()
は Unit と呼ばれ、ヌルタプルのように書かれています。値は 1 つだけです。また、値がなく、どの関数からも返されないVoid
ため、呼び出され
ません。Void
これを観察してください:Bool
には 2 つの値 (True
とFalse
)()
があり、値は 1 つ ( ()
) あり、Void
値はありません (存在しません)。それらは、要素が 2 つある / 1 つある / 要素がないセットのようなものです。値を格納するために必要な最小メモリは、それぞれ 1 ビット / ビットなし / 不可能です。つまり、 a を返す関数は、()
役に立たない結果値 (明らかな値) を返す可能性があります。Void
一方、結果が存在しないため、その関数は決して返されず、結果も返されないことを意味します。
「その値」に名前を付けたい場合は、関数が返すもので、決して返らないもの (はい、これはクレイジートークのように聞こえます) に名前を付け、それを下 (" _|_
"、逆 T のように記述) と呼びます。これは、例外、無限ループ、デッドロック、または「もう少し待つ」ことを表す可能性があります。(一部の関数は、パラメータの 1 つが下である場合にのみ下を返します。)
デカルト積/これらのタイプのタプルを作成すると、同じ動作が観察されます:
(Bool,Bool,Bool,(),())
2・2・2・1・1=6の異なる値があります。(Bool,Bool,Bool,(),Void)
値としてカウントしない限り、2・2・2・1・0=0要素を持つ集合{t,f}×{t,f}×{t,f}×{u}×{}_|_
のようなものです。