Skip to content

Commit

Permalink
Complete ex20
Browse files Browse the repository at this point in the history
  • Loading branch information
igrep committed Nov 17, 2024
1 parent 563e6f2 commit 98da662
Showing 1 changed file with 16 additions and 14 deletions.
30 changes: 16 additions & 14 deletions assets/20.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ ghci> :t (<$>) @IO
\ /
\ /
\_/
/\
IO Int
^^ ^^^
f b
/ \
IO Int
^^ ^^^
f b
```

ただし厳密に言えば、第1引数である関数はあくまでも`a -> b`という型の、「普通の関数」として**扱われる**、という点にはご注意ください。`<$>``IO`を返す関数`a -> IO b`に対して使うと、**型エラーにはならず**、妙な動作になってしまう場合があります。例えば、`putStrLn <$> getLine`とGHCi上で書いた場合...
Expand Down Expand Up @@ -102,7 +102,7 @@ putStrLn <$> getLine :: IO (IO ())
- `IO`に対する`<$>``(a -> b) -> IO a -> IO b`という型なので、
- 型変数`a``[Char]`、型変数`b``IO ()`が代入された。

そう、`IO ()`、実態は単なる普通の型なので、普通に型推論され、普通に型変数に代入されます。これまで扱ってきた`Integer``Bool``[a]`などと変わらない、普通の型なのです。
そう、`IO ()`自体は単なる普通の型なので、普通に型推論され、普通に型変数に代入されます。これまで扱ってきた`Integer``Bool``[a]`などと変わらない、普通の型なのです。

型と言うことは何らかの値を表しているはずです。これまではっきりと説明してきませんでしたが、一体`IO`型の値は何を表しているのでしょう?

Expand All @@ -116,7 +116,7 @@ putStrLn <$> getLine :: IO (IO ())

以上の通り、「関数オブジェクト」とよく似たこの`IO`は単なる値として扱えるので、Haskellにおける関数`a -> b`の結果`b`として返したり、`putStrLn <$> getLine`が返す`IO (IO ())`のように、`IO`を実行した結果として返すことができます。

では肝心な点、その結果として返された`IO`はどうやって実行することができるのでしょうか?簡潔に言うと、`main`関数やGHCiの中で評価することで(`>>=``>>`、あるいは`do`でつなげられて、その部分を通ったとき)初めて実行できます。ほかのプログラミング言語における「関数オブジェクト」と異なり、実行する専用の演算子[^execute-operator]はありません。ちょっと分かりづらいですね。
では肝心な点、その結果として返された`IO`はどうやって実行することができるのでしょうか?簡潔に言うと、`main`関数やGHCiの中で評価することで(`>>=``>>`、あるいは`do`でつなげられて、その部分を通ったとき)初めて実行できます。ほかのプログラミング言語における「関数オブジェクト」と異なり、実行する専専用の演算子[^execute-operator]はありません。ちょっと分かりづらいですね。

[^execute-operator]: JavaScriptやPythonの関数オブジェクトでは`()`、Rubyの`Proc`クラスのインスタンスやJavaの`Callable`を実装したオブジェクトでは`call`メソッドがその「実行する専用の演算子」に該当します。

Expand Down Expand Up @@ -167,7 +167,7 @@ main = putStrLn <$> getLine

というファイルを`not-echoed.hs`という名前で保存して実行してみても、やはりGHCiで実行した場合と同様に、`getLine`に当たる部分しか実行されません。

```haskell
```
shell> stack exec runhaskell not-echoed.hs
12345 # ここはユーザーによる入力
shell>
Expand All @@ -182,7 +182,7 @@ main = putStrLn <$> getLine

今度は以下のような型エラーが報告されるので、問題のある`putStrLn <$> getLine`は実行される間でもありません。

```haskell
```
shell> stack exec runhaskell not-echoed.hs
not-echoed.hs:2:8: error:
? Couldn't match type ‘IO ()’ with ‘()’
Expand All @@ -201,7 +201,7 @@ not-echoed.hs:2:8: error:

`+`などの二項演算子といった、2つ以上の引数を受け取る関数の引数として、`IO`などの「命令」の結果を渡す場合、`<$>`だけではできません(理由は後述します)。

例えばこれまで、下記のように`do`を使って書いていた場合を例としましょう:
例えばこれまで、下記のように`do`を使って書いていたコードを例としましょう:

```haskell
-- `getLine`を2回実行して、取得した2つの文字列を ++ で結合する
Expand Down Expand Up @@ -235,7 +235,7 @@ ghci> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
```

`Monad`, `Functor`と続いて`Applicative`というまた見知らぬ型クラスが登場しましたこれらの詳細は次の課題で行いますので今はまた`TypeApplications`で`f`を`IO`に置き換えましょう
`Monad`, `Functor`と続いて`Applicative`というまた見知らぬ型クラスが登場しましたこれらの詳細は次の課題で説明しますので今はまた`TypeApplications`で`f`を`IO`に置き換えましょう

```haskell
ghci> :set -XTypeApplications
Expand Down Expand Up @@ -302,6 +302,8 @@ return4 = do
return 4
```

`returnDoubler``<*>``return4`を組み合わせると、次のように振る舞います:

```haskell
ghci> returnDoubler <*> return4
Returning a function
Expand All @@ -313,7 +315,7 @@ Returning 4

以上から、`<*>`は、`<$>`が返した「関数を返す`IO`(命令)」と、`<*>`の右辺に渡したもう1つの`IO`(命令)を**続けて実行する**ための演算子であることがわかります。`<$>`は、あくまでも**1つの**命令の結果を関数に渡す役割である一方、`<*>`は、**2つ以上の**命令を続けて実行して関数に渡す、という点を覚えておいてください。

2つ以上」と書いたとおり、もちろん3つ以上の引数を受け取る関数に対しても`<*>`は使えます。例えば、今回の課題で定義しておくと便利であろう、「元金と金利(単位はパーセント)と年数を受け取って、年数後の元金を返す」関数を定義して、それに対して`<*>`を使ってみます。
2つ**以上**」と書いたとおり、3つ以上の引数を受け取る関数に対しても`<*>`は使えます。例えば、今回の課題で定義しておくと便利であろう、「元金と金利(単位はパーセント)と年数を受け取って、年数後の元金を返す」関数を定義して、それに対して`<*>`を使ってみます。

```haskell
-- Ref: https://support.microsoft.com/ja-jp/help/141695/xl-how-to-calculate-compound-interest
Expand Down Expand Up @@ -348,7 +350,7 @@ ghci> (\principal interestRate years -> principal * (1 + interestRate / 100.0) ^
110.25
```

もっと冗長だけどわかりやすいやり方、すなわち`do`記法を使ったやり方に立ち返ってもよいでしょう:
もっと冗長だけどわかりやすいやり方、すなわち`do`記法を使った方法に立ち返ってもよいでしょう:

```haskell
ghci> :{
Expand Down Expand Up @@ -390,7 +392,7 @@ ghci> (++) <$> getLine <$> getLine

というエラーです

このような型になった原因は`(++) <$> getLine`という式の型が`IO ([Char] -> [Char])`となった理由について説明したとおりです繰り返しになりますが`<$>`はあくまでも1つの命令を操作するための演算子であって`<*>`のように`IO ([Char] -> [Char])`と右辺が返す`IO [Char]`という2つの命令をつなげることができないのです
このような型になった原因は`(++) <$> getLine`という式の型が`IO ([Char] -> [Char])`となった理由について説明したとおりです繰り返しになりますが`<$>`はあくまでも1つの命令を操作するための演算子であって`<*>`のように`IO ([Char] -> [Char])`と右辺が返す`IO [Char]`という2つの命令をつなげることはできません

#### `>>=`との違い

Expand Down Expand Up @@ -469,4 +471,4 @@ ghci>
- NO =\> その関数が受け取る引数の数は?
- 1つだけ =\> `<$>`を使おう
- 2つ以上 =\> `<$>``<*>`を組み合わせて使おう
- どちらにしても、やっぱり`<$>``<*>`だと読みにくい! =\> `do`(と`return`)を使おう
- どちらにしても、やっぱり`<$>``<*>``>>=`だと読みにくい! =\> `do`(と`return`)を使おう

0 comments on commit 98da662

Please sign in to comment.