Haskell 学習記
なんでも知っているフリをするには Haskell を知らないというワケにはいかない気がするので Haskell を学ぶ。ツッコミどころがあれば誰でも何でも大歓迎。お気軽にどうぞ。
まずは環境構築。私の環境は macOS + Intel + Fish である。haskell install
でググったらトップに出てきた記事 Haskellの環境構築2023 によると、GHCup の指示通りに従うのが良さそうなのでそうする。
Unix 系 OS では
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
でインストールできるようなので実行する。
$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
Welcome to Haskell!
This script can download and install the following binaries:
* ghcup - The Haskell toolchain installer
* ghc - The Glasgow Haskell Compiler
* cabal - The Cabal build tool for managing Haskell software
* stack - A cross-platform program for developing Haskell projects (similar to cabal)
* hls - (optional) A language server for developers to integrate with their editor/IDE
ghcup installs only into the following directory,
which can be removed anytime:
/Users/paalon/.ghcup
Press ENTER to proceed or ctrl-c to abort.
Note that this script can be re-run at any given time.
-------------------------------------------------------------------------------
Detected fish shell on your system...
Do you want ghcup to automatically add the required PATH variable to "/Users/paalon/.config/fish/config.fish"?
[P] Yes, prepend [A] Yes, append [N] No [?] Help (default is "P").
A
-------------------------------------------------------------------------------
Do you want to install haskell-language-server (HLS)?
HLS is a language-server that provides IDE-like functionality
and can integrate with different editors, such as Vim, Emacs, VS Code, Atom, ...
Also see https://haskell-language-server.readthedocs.io/en/stable/
[Y] Yes [N] No [?] Help (default is "N").
Y
-------------------------------------------------------------------------------
Do you want to enable better integration of stack with GHCup?
This means that stack won't install its own GHC versions, but uses GHCup's.
For more information see:
https://docs.haskellstack.org/en/stable/yaml_configuration/#ghc-installation-customisation-experimental
If you want to keep stacks vanilla behavior, answer 'No'.
[Y] Yes [N] No [?] Help (default is "Y").
Y
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 27.6M 100 27.6M 0 0 8575k 0 0:00:03 0:00:03 --:--:-- 8575k
[ Info ] downloading: https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.8.yaml as file /Users/paalon/.ghcup/cache/ghcup-0.0.8.yaml
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 424k 100 424k 0 0 3670k 0 --:--:-- --:--:-- --:--:-- 3659k
[ Info ] Upgrading GHCup...
[ Warn ] No GHCup update available
System requirements
Note: On OS X, in the course of running ghcup you will be given a dialog box to install the command line tools. Accept and the requirements will be installed for you. You will then need to run the command again.
On Darwin M1 you might also need a working llvm installed (e.g. via brew) and have the toolchain exposed in PATH.
Press ENTER to proceed or ctrl-c to abort.
Installation may take a while.
[ Info ] downloading: https://downloads.haskell.org/~ghc/9.4.8/ghc-9.4.8-x86_64-apple-darwin.tar.xz as file /Users/paalon/.ghcup/cache/ghc-9.4.8-x86_64-apple-darwin.tar.xz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 179M 100 179M 0 0 9643k 0 0:00:19 0:00:19 --:--:-- 9901k
[ Info ] verifying digest of: ghc-9.4.8-x86_64-apple-darwin.tar.xz
[ Info ] Unpacking: ghc-9.4.8-x86_64-apple-darwin.tar.xz to /Users/paalon/.ghcup/tmp/ghcup-d53f019aa9d89ede
[ Info ] Installing GHC (this may take a while)
[ Info ] Merging file tree from "/Users/paalon/.ghcup/tmp/ghcup-42328ccebcd7f0a2/Users/paalon/.ghcup/ghc/9.4.8" to "/Users/paalon/.ghcup/ghc/9.4.8"
[ Info ] GHC installation successful
[ Info ] downloading: https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.8.yaml as file /Users/paalon/.ghcup/cache/ghcup-0.0.8.yaml
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
[ Info ] GHC 9.4.8 successfully set as default version
[ Info ] downloading: https://downloads.haskell.org/ghcup/unofficial-bindists/cabal/3.10.3.0/cabal-install-3.10.3.0-x86_64-apple-darwin.tar.xz as file /Users/paalon/.ghcup/cache/cabal-install-3.10.3.0-x86_64-apple-darwin.tar.xz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 5581k 100 5581k 0 0 8980k 0 --:--:-- --:--:-- --:--:-- 8973k
[ Info ] verifying digest of: cabal-install-3.10.3.0-x86_64-apple-darwin.tar.xz
[ Info ] Unpacking: cabal-install-3.10.3.0-x86_64-apple-darwin.tar.xz to /Users/paalon/.ghcup/tmp/ghcup-293c36cdcb00d017
[ Info ] Installing cabal
[ Info ] Cabal installation successful
Config file path source is default config file.
Config file not found: /Users/paalon/.cabal/config
Writing default configuration to /Users/paalon/.cabal/config
Downloading the latest package list from hackage.haskell.org
Package list of hackage.haskell.org has been updated.
The index-state is set to 2024-06-23T04:37:08Z.
[ Info ] downloading: https://downloads.haskell.org/~ghcup/unofficial-bindists/haskell-language-server/2.7.0.0/haskell-language-server-2.7.0.0-x86_64-apple-darwin.tar.xz as file /Users/paalon/.ghcup/cache/haskell-language-server-2.7.0.0-x86_64-apple-darwin.tar.xz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 169M 100 169M 0 0 8498k 0 0:00:20 0:00:20 --:--:-- 8254k
[ Info ] verifying digest of: haskell-language-server-2.7.0.0-x86_64-apple-darwin.tar.xz
[ Info ] Unpacking: haskell-language-server-2.7.0.0-x86_64-apple-darwin.tar.xz to /Users/paalon/.ghcup/tmp/ghcup-0908d99d67f9a8aa
[ Info ] Installing HLS
[ Info ] Merging file tree from "/Users/paalon/.ghcup/tmp/ghcup-bab2e2f7367fcc33/Users/paalon/.ghcup/hls/2.7.0.0" to "/Users/paalon/.ghcup/hls/2.7.0.0"
[ Info ] HLS installation successful
[ Info ] This is just the server part of your LSP configuration. Consult the README on how to
[ ... ] configure HLS, your project and your LSP client in your editor:
[ ... ] https://haskell-language-server.readthedocs.io/en/stable/
[ Info ] downloading: https://downloads.haskell.org/~ghcup/unofficial-bindists/stack/2.15.5/stack-2.15.5-osx-x86_64.tar.gz as file /Users/paalon/.ghcup/cache/stack-2.15.5-osx-x86_64.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 8493k 100 8493k 0 0 9494k 0 --:--:-- --:--:-- --:--:-- 9490k
[ Info ] verifying digest of: stack-2.15.5-osx-x86_64.tar.gz
[ Info ] Unpacking: stack-2.15.5-osx-x86_64.tar.gz to /Users/paalon/.ghcup/tmp/ghcup-1415566a5b57c92f
[ Info ] Installing stack
[ Info ] Stack installation successful
[ Info ] Stack manages GHC versions internally by default. To improve integration, please visit:
[ ... ] https://www.haskell.org/ghcup/guide/#stack-integration
[ ... ]
[ ... ] Also check out:
[ ... ] https://docs.haskellstack.org/en/stable/yaml_configuration
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 764 100 764 0 0 1134 0 --:--:-- --:--:-- --:--:-- 1135
===============================================================================
OK! /Users/paalon/.config/fish/config.fish has been modified. Restart your terminal for the changes to take effect,
or type ". /Users/paalon/.ghcup/env" to apply them in your current terminal session.
===============================================================================
All done!
To start a simple repl, run:
ghci
To start a new haskell project in the current directory, run:
cabal init --interactive
To install other GHC versions and tools, run:
ghcup tui
If you are new to Haskell, check out https://www.haskell.org/ghcup/steps/
いくつか質問を聞かれるので、答えておく。読めば分かる親切な構成になっていて良い。
アンインストールしたければ
-
~/.ghcup
を削除する -
~/.config/fish/config.fish
に追加された# ghcup-env
のコメントを持つ一行を消す(私は Fish なのでこれだが他のシェルなら適当な他の設定ファイル)
をすれば良いだろう。
シェルを再起動して以下のファイルを作って
-- Hello.hs
module Main where
main :: IO ()
main = putStrLn "Hello!"
コンパイル&実行してみる。
$ ghc Hello.hs
[1 of 2] Compiling Main ( Hello.hs, Hello.o )
[2 of 2] Linking Hello
ld: warning: ignoring duplicate libraries: '-lm'
$ ./Hello
Hello!
正常にインストールはできたようだ。
それでは教材を選ぶ。ちなみにハローワールドと Fizz Buzz とライプニッツ級数の有限打ち切りを計算するプログラムは調べたりコピペしたりしながら書いたことはある。なんで動いているのか分からないとか、なんでこういう仕様なのか分からないという状態ではなく、隅から隅まで Haskell を学んでいきたいと思っている。
私がそれなりに詳しい言語は
- Julia
- C++
- C
- Python
- JavaScript
- TypeScript
という感じ。触ったことある言語は
- Scheme
- Racket
- Nim
- Rust
- Go
などなど。OCaml とか SML とか Haskell は表層しか理解していないと思っている。
https://lotz84.github.io/haskell/tutorial.html を見て良さげな https://soupi.github.io/rfc/writing_simple_haskell/ から始めることにする。
6 ページ目。
最初は上記のハローワールドの説明。
-
::
は "type of" の意味。 -
=
は等値の意味。 -
putStrLn
の型はString -> IO ()
-
main
の型はIO ()
なんでも型がつく静的言語というところだろうか。わかりやすくて良い。
7 ページ目。
- 型
IO a
はこれはサブルーチン(実行されるとき、入出力アクションを実行するかもしれず、最後に型a
の値を返す)の記述である。 -
main
はプログラムのエントリーポイントの名前である。 - Haskell runtime は
main
を見つけて実行する。
C/C++ だったら main 関数の返り値の型を int
とだけ記述したり、Go や Rust の main では何も指定しないのとは対照的である。
8 ページ目。
module Main where
main :: IO ()
main = do
putStrLn "Hello! What is your name?"
name <- getLine
let out = "Nice to meet you, " ++ name ++ "!"
putStrLn out
$ ghc Hello.hs
[1 of 2] Compiling Main ( Hello.hs, Hello.o ) [Source file changed]
[2 of 2] Linking Hello [Objects changed]
ld: warning: ignoring duplicate libraries: '-lm'
$ ./Hello
Hello! What is your name?
Paalon
Nice to meet you, Paalon!
入出力の例だな。
9 ページ目。
- Haskell は indentation sensitive である。
- 何らかの表現の一部のコードはその表現の始まりよりもインデントされていなければいけない。
インデント数はなんでもいいのだろうか?
10 ページ目。
-
do
は特別な文法(sequence 入出力アクションをさせてくれる)である。 -
getLine
の型はIO String
である。 -
IO String
の意味は…(以下略) -
getLine
は標準入力から一行を取り出してString
型の値を生成する。
関数の呼び出しには括弧はいらないということだろう。今までやってきた言語との一番の違いはここな気がする。
... <- ...
と ... = ...
と let ... = ...
の違いがまだ分かっていない。次のページに説明があるだろう。進もう。
11 ページ目。
-
getLine
の型はIO String
である。 -
name
の型はString
である。 -
<-
は特別な文法(do
記法の中でのみ現れる)である。 -
<-
は「サブルーチンを実行し、その結果得られた値を<-
の左側にある名前に結び束ねる」ということを意味する。 -
let <name> = <expr>
は「残りのdo
ブロックにおいて、<name>
は<expr>
と相互に交換可能である」ということを意味する。 -
do
記法中では、let
はin
とともに使われなくても良い。
副作用のある処理の結果を代入するときには <-
を使えば良いのだろうか?単なる =
と let
付きの =
の違いがよく分からない。最後の let
と in
の話は何のことを言っているのか分からない。だってまだ in
が出てきていないからね。<-
と =
の書き間違いにはしばらく苦しまされそうだ。
12 ページ目。よくあるエラーその1。
module Main where
main :: IO ()
main = do
putStrLn "Hello! What is your name?"
let out = "Nice to meet you, " ++ getLine ++ "!"
putStrLn out
$ ghc Hello.hs
[1 of 2] Compiling Main ( Hello.hs, Hello.o ) [Source file changed]
Hello.hs:6:37: error:
• Couldn't match expected type: [Char]
with actual type: IO String
• In the first argument of ‘(++)’, namely ‘getLine’
In the second argument of ‘(++)’, namely ‘getLine ++ "!"’
In the expression: "Nice to meet you, " ++ getLine ++ "!"
|
6 | let out = "Nice to meet you, " ++ getLine ++ "!"
| ^^^^^^^
[Char]
が来るべきところに IO String
が来たというエラーかな?[Char]
は多分 Char
の配列型という意味だろう。
13 ページ目。よくあるエラーその1:String
の代わりに IO String
を使ってしまうこと。
- 註:
String
はtype String = [Char]
と定義されている。 - Haskell は期待される型
String
とgetLine
の型IO String
が一致させられないと言っている。 -
IO a
とa
は違う型である。
分かる。
14 ページ目。よくあるエラーその2。
module Main where
main :: IO ()
main = do
putStrLn "Hello! What is your name?"
name <- getLine
putStrLn "Nice to meet you, " ++ name ++ "!"
$ ghc Hello.hs
[1 of 2] Compiling Main ( Hello.hs, Hello.o ) [Source file changed]
Hello.hs:7:3: error:
• Couldn't match type ‘[]’ with ‘IO’
Expected: IO ()
Actual: [()]
• In a stmt of a 'do' block:
putStrLn "Nice to meet you, " ++ name ++ "!"
In the expression:
do putStrLn "Hello! What is your name?"
name <- getLine
putStrLn "Nice to meet you, " ++ name ++ "!"
In an equation for ‘main’:
main
= do putStrLn "Hello! What is your name?"
name <- getLine
putStrLn "Nice to meet you, " ++ name ++ "!"
|
7 | putStrLn "Nice to meet you, " ++ name ++ "!"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Hello.hs:7:36: error:
• Couldn't match type ‘Char’ with ‘()’
Expected: [()]
Actual: String
• In the first argument of ‘(++)’, namely ‘name’
In the second argument of ‘(++)’, namely ‘name ++ "!"’
In a stmt of a 'do' block:
putStrLn "Nice to meet you, " ++ name ++ "!"
|
7 | putStrLn "Nice to meet you, " ++ name ++ "!"
| ^^^^
Hello.hs:7:44: error:
• Couldn't match type ‘Char’ with ‘()’
Expected: [()]
Actual: String
• In the second argument of ‘(++)’, namely ‘"!"’
In the second argument of ‘(++)’, namely ‘name ++ "!"’
In a stmt of a 'do' block:
putStrLn "Nice to meet you, " ++ name ++ "!"
|
7 | putStrLn "Nice to meet you, " ++ name ++ "!"
|
さっきよりも長いエラーが出た。これはちょっと分からないぞ。
- 1つ目のエラーは型
[]
を型IO
に合わせられないというエラー。IO ()
を期待したが[()]
が来た、と。 - 2つ目のエラーは型
Char
を型()
に合わせられないというエラー。[()]
を期待したがString
が来た、と。 - 3つ目のエラーは型
Char
を型()
に合わせられないというエラー。[()]
を期待したがString
が来た、と。
とりあえず型 ()
は C/C++ でいうところの void
、Julia でいうところの nothing
、 Python でいうところの None
、 JavaScript でいうところの undefined
だな。
1つ目のエラーは何も返さない IO
を期待したが、()
の配列が来たということか。
関係ないけど In an equation for ‘main’:
って書いてあるから main = do ...
の表現は等式 (equation) というようだ。
分からないので2つ目と3つ目のエラーを観察する。name
が String
なのは私の期待通りであるが、Haskell の期待通りではないらしい。Haskell は [()]
を期待しているらしい。うーん分からない。++
の仕様が分かっていないせいだろうか。
答えを見ずに悩むのはこれくらいにして、次のページに進もう。
15 ページ目。よくあるエラーその2。関数適用は演算子適用に先行する。(Common Error #2 - Function application precedes operator application)
フランス語だと関数のことを application
と言うのでフランス人が見たら発狂しそうな一文である。
- 文字列表現の周りに丸括弧が必要である。
putStrLn ("Nice to meet you, " ++ name ++ "!")
なるほど。括弧は省略できるだけでないわけではないようだ。それでも省略できるときは省略したがるスタイルなんだろう。関数名と括弧の間にスペースを入れるのはこだわりなのかな?他の言語じゃかなり嫌われるので。
上記の通りに括弧を入れると正しく動作した。関数と括弧の間のスペースを消して、コンパイル&実行してみても何も文句を言われずに動作したので、消しても問題はないようだ。
16 から 21 ページ。TODO アプリの設計について。
type Item = String
type Items = [Item]
22 ページ目。TODO: 関数としてのアクション
-- Returns a new list of Items with the new item in it
addItem :: Item -> Items -> Items
-- Returns a string representation of the items
displayItems :: Items -> String
-- Returns a new list of items or an error message if the index is out of bounds
removeItem :: Int -> Items -> Either String Items
- 失敗する可能性を記すのに
Either
が使われる。
演算子 ->
の結合順位が分からない。結合法則は成り立たないので結合順位があるはずだ。3分くらい眺めていても意味が分からなかったが、分かってきた。デカルト積を表す記号がないようだ。->
でデカルト積を無理やり表現している。addItem :: Item -> Items -> Items
は displayItems
はそのままの意味で、removeItem :: Int -> Items -> Either String Items
は ->
は左から優先的に結合することになっているのだろう。デカルト積を表す記号がないとヤバそうだが、大丈夫だろうか?
23 ページ目。addItem。
-- Returns a new list of Items with the new item in it
addItem :: Item -> Items -> Items
addItem item items = item : items
a : b
で a
と b
を結合した配列を表すようだ。
24 ページ目。displayItems。
-- Returns a string representation of the items
displayItems :: Items -> String
displayItems items =
let
displayItem index item = show index ++ " - " ++ item
reversedList = reverse items
displayedItemsList = zipWith displayItem [1..] reversedList
in
unlines displayedItemsList
-
hoogle を使って
zipWith
とreverse
とunlines
を検索して詳しく調べよ。 - Haskell は値を必要なときだけ評価する(例えば、ユーザーに何かをプリントするために評価される必要があるとき)。
- この動作により、
[1..]
のような無限リストに対しても動作する関数を書けるようになり、必要な部分だけを評価する。 - Haskell の評価については、このガイドで詳しく読むことができる。
前に言っていた in
が出てきた。reverse
は配列を逆向きにするのだろう。zipWith
は配列の長さが一致していなくてもいい感じに割り当てて評価するんだろう。unlines
はよく分からない。in
も分からない。
分からないので hoogle を使って調べる。zipWith
で調べるといっぱい異なる型ごとのドキュメントが出てくる。とりあえず先頭に出てきた zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
をクリックする。
zipWith
はだいたい想像通りのようだ。Julia で言うなら
zipwith(f, xs, ys) = [f(x, y) for (x, y) in zip(xs, ys)]
というところか。unlines
を調べる。
なるほど。Julia なら
unlines(x::Vector{String}) = join(x .* '\n')
という意味だ。ついでに reverse
も調べたが想像通りの意味だった。あとは let
と in
の使い方だけ分からない。評価についてのガイドは今は読まなくてもいいだろう。次のページに進もう。
25 ページ目。User Interaction。
let
と in
についての説明が来るかと思ったら来なかった。まあ後で調べることにする。
-
removeItem
は今は飛ばして、ユーザーインタラクションの機能を加えよう。
-- Takes a list of items
-- Interact with the user
-- Return an updated list of items
interactWithUser :: Items -> IO Items
26 ページ目。
- 1行読んで、それをアイテムとして扱い、それをリストに加えて、新しいアイテム群を表示する。
interactWithUser :: Items -> IO Items
interactWithUser items = do
putStrLn "Enter an item to add to your todo list:"
item <- getLine
let newItems = addItem item items
putStrLn "Item added.\n"
putStrLn "The List of items is:"
putStrLn (displayItems newItems)
pure newItems
-
do
記法の最後の行は計算の結果である。 - この場合では、その結果は型
IO Items
の値となる必要がある。 - しかし、
newItems
は型Items
を持つ。 - なので型
a -> IO a
を持つpure
を使う。 -
pure
は入出力をしないa
を生成するサブルーチンを作成する。
コードをぱっと見ただけだと、pure
が謎だったが、説明を読んで理解した。しかし、IO
ありと IO
なしを常に意識しなければならず、認知コストが上がりそうだ。
27 ページから 28 ページ。
module Main where
type Item = String
type Items = [Item]
-- Returns a new list of Items with the new item in it
addItem :: Item -> Items -> Items
addItem item items = item : items
-- Returns a string representation of the items
displayItems :: Items -> String
displayItems items =
let
displayItem index item = show index ++ " - " ++ item
reversedList = reverse items
displayedItemsList = zipWith displayItem [1..] reversedList
in
unlines displayedItemsList
-- Takes a list of items
-- Interact with the user
-- Return an updated list of items
interactWithUser :: Items -> IO Items
interactWithUser items = do
putStrLn "Enter an item to add to your todo list:"
item <- getLine
let newItems = addItem item items
putStrLn "Item added.\n"
putStrLn "The List of items is:"
putStrLn (displayItems newItems)
pure newItems
main :: IO ()
main = do
putStrLn "TODO app"
let initialList = []
interactWithUser initialList
putStrLn "Thanks for using this app."
今までのを繋ぎ合わせる。詳細な例は書いていないが多分こういうことだろう。コンパイルして実行したら動いた。想定読者がよく分かっていないけれど、プログラミング初心者には不親切だろうな。
29 ページから 31 ページ。イテレーションと状態。
- Todo リストを返す代わりに、
interactWithUser
にフィードバックする。
interactWithUser :: Items -> IO ()
interactWithUser items = do
putStrLn "Enter an item to add to your todo list:"
item <- getLine
let newItems = addItem item items
putStrLn "Item added.\n"
putStrLn "The List of items is:"
putStrLn (displayItems newItems)
interactWithUser newItems
コードを上記の通り編集して実行して期待の動作を確認。
32 ページから 33 ページ。
data Command
= Quit
| DisplayItems
| AddItem String
- 新しい ADT を作成して、ユーザーコマンドの可能性をモデル化する。
ADT が何のイニシャリズムか分からなかったが、調べたら algebraic data type のイニシャリズムのようだ。イントロダクションならそのくらい、略さないで言ってもらいたいという愚痴。プログラムの中身は分かった。
34 ページ。
- さっきのデータ型へユーザーコマンドをパースする。
- これは失敗するかもしれない。
parseCommand :: String -> Either String Command
parseCommand line = case words line of
["quit"] -> Right Quit
["items"] -> Right DisplayItems
"add" : "-" : item -> Right (AddItem (unwords item))
_ -> Left "Unknown command."
_
はそれ以外という意味だろう。words
は文字列をスペースでの区切りで分割する関数らしい。case ... of ...
は switch
的なものだろう。"add" : "-" : item
のところはよく分からない。Right
と Left
も何をしているのか分からない。次に進もう。
35 ページ。
interactWithUser :: Items -> IO ()
interactWithUser items = do
putStrLn "Commands: quit, items, add - <item to add>"
line <- getLine
case parseCommand line of
Right DisplayItems -> do
putStrLn "The List of items is:"
putStrLn (displayItems items)
interactWithUser items
Right (AddItem item) -> do
let newItems = addItem item items
putStrLn "Item added."
interactWithUser newItems
Right Quit -> do
putStrLn "Bye!"
pure ()
Left errMsg -> do
putStrLn ("Error: " ++ errMsg)
interactWithUser items
Right
と Left
以外のところ以外は不思議なところはないかな。次に進もう。
36 ページ目。実行できた。Right
と Left
の説明はしてくれないようだ。後で調べよう。
37 ページ目。
data Command
...
| Help
parseCommand :: String -> Either String Command
parseCommand line = case words line of
...
["help"] -> Right Help
_ -> Left "Unknown command."
interactWithUser :: Items -> IO ()
interactWithUser items = do
line <- getLine
case parseCommand line of
Right Help -> do
putStrLn "Commands: help, quit, items, add - <item to add>"
interactWithUser items
...
特に言及することはなさそう。
38 ページ目。
-- Returns a new list of items or an error message if the index is out of bounds
removeItem :: Int -> Items -> Either String Items
removeItem reverseIndex allItems =
impl (length allItems - reverseIndex) allItems
where
impl index items =
case (index, items) of
(0, item : rest) ->
Right rest
(n, []) ->
Left "Index out of bounds."
(n, item : rest) ->
case impl (n - 1) rest of
Right newItems ->
Right (item : newItems)
Left errMsg ->
Left errMsg
impl
が出てきた。何をしているのかよくわからない。どれが関数でどれが値なのか分からないという感じ。飛ばそう。次に進む。
39 ページ目。
data Command
...
| Done Int
parseCommand :: String -> Either String Command
parseCommand line = case words line of
...
["done", idxStr] ->
if all (\c -> elem c "0123456789") idxStr
then Right (Done (read idxStr))
else Left "Invalid index."
_ -> Left "Unknown command."
interactWithUser :: Items -> IO ()
interactWithUser items = do
line <- getLine
case parseCommand line of
Right Help -> do
putStrLn "Commands: help, quit, items, add - <item to add>, done <item index>"
interactWithUser items
Right (Done index) -> do
let result = removeItem index items
case result of
Left errMsg -> do
putStrLn ("Error: " ++ errMsg)
interactWithUser items
Right newItems -> do
putStrLn "Item done."
interactWithUser newItems
...
急に説明がなくなって、コードだけになってきた。if ... then ... else ...
が出てきた。\c
は何を表しているのだろうか?そのくらいかな? all
とかも分かっていないか。次に進もう。
40 ページから 43 ページでおしまい。
最後らへんで急に説明が何もなくなってしまった。入門資料としてはあまりよくなかったかもしれない。まあでも、分からんポイントが出てきたので良しとしよう。次はどの資料を見ようか。
いろんな入門を書いているとほほのHaskell入門 を見ることにする。
分かっているところは飛ばしてカラカラと見る。
予約語をそういえば見ていなかった。
case class data default deriving
do else foreign if import
in infix infixl infixr instance
let module newtype of then
type where _
ふむふむ。
- インラインコメントは
--
で始める。 - ブロックコメントは
{- ブロックコメント -}
である。 -
{ 式1; 式2; 式3 }
として複数の式を1つの式にできる。 - インデントと改行でブロックの波括弧とセミコロンを省略できる。
主な型
-
Bool
真偽値True
,False
-
Char
文字 -
String
文字列[Char]
である。 -
Int
30 bits 以上の整数 -
Integer
多倍長整数 -
Float
32 bits 浮動小数点数 -
Double
64 bits 浮動小数点数 -
[Int]
リスト -
(Int, Char)
タプル -
Int -> Int
Int
を受け取りInt
を返す関数 -
Int -> Int -> Double
Int
を2つ受け取りDouble
を返す関数 -
a
任意の型 -
[a]
任意の型のリスト
デカルト積はないのにタプルはあるらしい。謎だな。そして a
は任意の型だったらしい。衝撃。
変数名には英数字とアンダーバーとシングルクォートが使える。小文字始まりでなければならない。
リストはリストであってシリアライズされたデータである配列にはなっていないらしい。配列はあるんだろうか。
-
[1, 2, 3]
整数リスト -
[1..3]
[1, 2, 3]
と同じ意味。 -
[1, 3...9]
[1, 3, 5, 7]
と同じ意味。(なぜか分からない) -
[3, 2..0]
[3, 2, 1, 0]
と同じ意味。(なぜか分からない) -
['a', 'b', 'c']
文字リスト"abc"
と同じ意味。 -
['a'..'c']
['a', 'b', 'c']
と同じ意味。 -
["Red", "Green", "Blue"]
文字列リスト -
[1, 2, 3] !! 2
0-based indexing で 2 番目の要素を取り出す。つまり3
である。 -
[1, 2] ++ [3, 4]
リストの連結。Julia で言えばcat
かappend!
的な感じだろう。 -
length [1, 2, 3]
リストの長さ。Julia で言えばlength
-
head [1, 2, 3]
リストの先頭。Julia で言えばfirst
-
last [1, 2, 3]
リストの最後。Julia で言えばlast
-
tail [1, 2, 3]
リストの先頭を除いたもの。Julia で言えば[1, 2, 3][begin+1:end]
-
init [1, 2, 3]
リストの最後を除いたもの。Julia で言えば[1, 2, 3][begin:end-1]
-
take 2 [1, 2, 3]
先頭から2つの要素からなるリストを返す。Julia なら[1, 2, 3][1:2]
-
takeWhile (<3) [1, 2, 3]
条件に合致する要素からなるリストを返す。Julia ならfilter(x -> x < 3, [1, 2, 3])
-
drop 2 [1, 2, 3]
先頭から2つの要素を除いた要素からなるリストを返す。Julia なら[1, 2, 3][begin+2:end]
-
dropWhilte (<3) [1, 2, 3]
条件に合致しない要素からなるリストを返す。Julia ならfilter(!(x -> x < 3), [1, 2, 3])
-
reverse [1, 2, 3]
リストを逆順にする。Julia ならreverse
かreverse!
-
map (*2) [1, 2, 3]
リストに対して関数を適用する。Julia なら[1, 2, 3] * 2
か[1, 2, 3] .* 2
かmap(x -> x * 2, [1, 2, 3])
など。
以下は等価。
[1, 2, 3]
1:[2, 3]
1:2:[3]
1:2:3:[]
Haskell の例
s = [x * x | x <- [1..5]] -- [1, 4, 9, 16, 25]
は Julia なら
s = [x * x for x = 1:5]
Haskell の例
s = [x * x | x <- [1..5], x /= 3]
は Julia なら
s = [x * x for x = filter(x -> x ≠ 3, 1:5)]
タプルはリストと似ていますが、要素は同じ型である必要はありません。
逆に言えば、リストは型が同じでなければならないようだ。これはかなりきつそう。
(1, 2, 3)
(1, 'a', "ABC")
要素数が 0個のタプルはユニット(unit)と呼ばれます。
func = return ()
ということは要素数が0個のタプルであるユニットを Julia でいうところの nothing
として使っているということだろう。つまり Julia で言うところの ()
と nothing
の区別はないということだろう。これもきつそう。
タプルの要素の取り出し
fst (1, 'a', "ABC") -- 1
snd (1, 'a', "ABC") -- 'a'
(_, _, x) = (1, 'a', "ABC") -- binds x to "ABC"
演算子
expr1 + expr2 -- 加算 expr1 - expr2 -- 減算 expr1 * expr2 -- 乗算 expr1 / expr2 -- 除算 expr1 `div` expr2 -- 除算(-∞方向に丸める) expr1 `mod` expr2 -- 除算(`div`)の余り expr1 `rem` expr2 -- 除算(`quot`)の余り expr1 `quot` expr2 -- 除算(0方向に丸める) expr1 ^ expr2 -- 累乗(expr2は整数) expr1 ^^ expr2 -- 累乗(expr1は実数、expr2は整数) expr1 ** expr2 -- 累乗(expr1もexpr2も実数) expr1 == expr2 -- 等しければ expr1 /= expr2 -- 等しくなければ expr1 < expr2 -- 大きければ expr1 <= expr2 -- 以上であれば expr1 > expr2 -- 小さければ expr1 >= expr2 -- 以下であれば bool1 && bool2 -- かつ bool1 || bool2 -- または not bool -- 否定
数値の演算子は Julia と比較すると若干しょぼそうだがだいたいいいだろう。多分 Nim のようになんでも二項演算子を定義できるのだろう。そこは良さそう。後ろの方は今まで見たことないから学びがいがある。
list !! index -- リストの index番目の要素 list1 ++ list2 -- リストを連結(文字列連結にも使用可) value : list -- [value] ++ list と同義 expr `elem` list -- expr が list に含まれていれば expr `notElem` list -- expr が list に含まれていなければ func $ expr -- func ( expr ) と等価 func $! expr -- func ( expr ) と等価 (exprを即時評価する) func1 . func2 -- 関数合成 expr1 `seq` expr2 -- 正格評価を行う(遅延評価を行わない) var <- action -- アクションから値を取り出す func =<< action -- アクションから値を取り出し関数に引き渡す action >>= func -- アクションから値を取り出し関数に引き渡す stmt1 >> do {stmt2} -- do { stmt1; stmt2 } と等価
アクション (action) というのは厳密な用語のようだ。アクションの連鎖を明示的に書くならば
stmt1 >> stmt2 >> stmt3 >> stmt4
みたいな感じになるということだろう。
二項演算子を丸括弧で囲むと関数として使える。逆に2つの引数を持つ関数をバッククオート (U+0060) で囲むと二項演算子として使える。
関数
add x y = x + y
上記の下で
main = print (add 3 5)
main = print $ add 3 5
は等価である。
二項演算子定義
以下の記号からなる文字列として二項演算子を定義できる。
# $ % & * + . / < = > ? @ \ ^ | - ~
-
infix
結合なし -
infixl
左結合 -
infixr
右結合
を使って定義した二項演算子の優先度を 0 から 9 までで指定できる。
ラムダ式
\arg -> expr
\(arg1, arg2) -> expr
バックスラッシュはラムダ式の記法だったようだ。
パターンマッチ
関数を引数の値によって別々に定義できる。
func 1 = "One"
func 2 = "Two"
func 3 = "Three"
main = print $ func 1
これは Julia にないし、Rust にもない?のかな。便利だと思う。
ガード条件
foo x
| x == 1 = "One"
| x == 2 = "Two"
| x == 3 = "Three"
| otherwise = "More..."
main = putStrLn $ foo 2
これも Julia にないし、いい機能だと思う。
関数合成
f n = n * 2
g n = n * 3
h n = n * 4
-- fn n = f(g(h(n)))
-- fn n = (f . g . h) n
fn = (f . g . h)
main = print $ fn 5
これは Julia で言うところの ∘
である。
f(n) = 2n
g(n) = 3n
h(n) = 4n
fn = f ∘ g ∘ h
println(fn(5))
引数補足
関数の引数は @ を用いて複数の形式で受け取ることができます。下記では、引数の文字列全体を str として、また、先頭の文字を x、残りの文字を xs として受け取ることができます。
func str@(x:xs) = do print str -- "ABCDE" print x -- 'A' print xs -- "BCDE" main = do func "ABCDE"
ほぇ〜。便利かどうかは使って慣れてみないと分からないかな。
do 文
do は式や指定した式を処理します。式には ブロック を指定することもできます。
main = do { print "A"; print "B"; print "C" }
ブロックは レイアウト を用いて下記の様に記述することもできます。
main = do print "A" print "B" print "C"
Scheme の begin みたいなものかと。
let 文
let は変数と値をバインド(束縛)します。一度バインドした変数は他の値に変更することはできません。
main = do let msg = "Hello" putStrLn msg
in ... を用いると、バインドした変数は in ... の中だけで有効な変数となります
area_of_circle r = let pi = 3.14 in do r * r * pi main = print $ area_of_circle 1.23
うーん。in
の意味が分からない。他の説明を見ようかな。
if 文 はまあいいだろう。
case 文。
case expr of
pattern1 -> expr1
pattern2 -> expr2
_ -> expr3
case ... of ... -> ... ... -> ... _ -> ...
ね。Julia にはないので(愚直に if ... elseif ... else ...
を書く)たまに欲しくなる。
where 文
where 文は、変数や補助関数を局所的に定義します。
main = print $ add x y where x = 123 y = 456 add x y = x + y
うーんと、where はなくても良いということだろうか?分からない。名前を散らかさないようにスコープを作るのに便利なのかな?
import 文
import [qualified] ModuleName [as AliasName] [[hiding] (name, ...)]
使いながら覚えないとよく分からなさそう。モジュールがあることは良いことだ。
ループ
loop 0 action = return ()
loop n action = do
action
loop (n - 1) action
main = loop 10 $ putStrLn "Hello"
import Control.Monad
main = do
replicateM_ 3 $ putStrLn "Hello"
データ型
data TypeName =
Constructor1 { fieldLabel1a :: Type1a, fieldLabel1b :: Type1b, ... }
[ | Constructor2 { fieldLabel2a :: Type2a, fieldLabel2b :: Type2b, ... } ]...
[deriving (TypeClass, ...)]
例をもうちょっと見ないと分からないかも。
列挙型
data Color = Red | Green | Blue deriving (Show, Eq)
main = do
let x = Red
let y = Green
if x == y then print "Equal" else print "Not Equal"
なんか Rust っぽくなってきた。Show
とか Eq
を型クラスというらしい。Julia で言えば抽象型、C++ で言えば継承やコンセプト、Rust でいうところの derive
か?
タプル型
data Point = Point Int Int deriving Show
addPoint (Point x1 y1) (Point x2 y2) = Point (x1 + x2) (y1 + y2)
main = do
let a = Point 100 200
b = Point 300 400
c = addPoint a b
print c -- Point 400 600
普通の構造体のことだろう。
直和型
data Figure = Rect { x1, y1, x2, y2 :: Int }
| Circle { x, y, r :: Int }
deriving Show
main = do
let a = Rect { x1 = 100, y1 = 100, x2 = 200, y2 = 200 }
b = Circle { x = 100, y = 100, r = 100 }
print a -- Rect {x1 = 100, y1 = 100, x2 = 200, y2 = 200}
print b -- Circle {x = 100, y = 100, r = 100}
Julia でいうところの
const Figure = Union{Rect, Circle}
または
abstract type Show end
abstract type Figure <: Show end
struct Rect <: Figure end
struct Circle <: Figure end
というところか。
新型定義
newtype Pixel = Pixel Int deriving Show
main = do
let a = Pixel 300
print a
これはよく分からない。
型シノニム
type Person = (Name, Address)
type Name = String
type Address = None | Addr String
これは C++ でいうところの using
や Julia の const A = B
ということだろう。
型クラス
class Foo a where
foo :: a -> String
instance Foo Bool where
foo True = "Bool: True"
foo False = "Bool: False"
instance Foo Int where
foo x = "Int: " ++ show x
instance Foo Char where
foo x = "Char: " ++ [x]
main = do
putStrLn $ foo True -- Bool: True
putStrLn $ foo (123::Int) -- Int: 123
putStrLn $ foo 'A' -- Char: A
あまり不思議なところはない。
Maybeモナド
fn :: Int -> Maybe String
fn n
| n == 1 = Just "One"
| n == 2 = Just "Two"
| otherwise = Nothing
main = do
print $ fn 1 -- Just "One"
print $ fn 2 -- Just "Two"
print $ fn 3 -- Nothing
nothing は Nothing
で在るらしい。
Functor 型クラス
map
の汎用版である fmap
を持つ。
fn n = n * 2
main = do
print $ fmap fn [1, 2, 3] -- [2, 4, 6]
print $ fmap fn Nothing -- Nothing
print $ fmap fn (Just 5) -- Just 10
print $ fmap fn (2, 3) -- (4, 6)
fn <$> x
は fmap fn x
の別記法。
fn n = n * 2
main = do
print $ fn <$> [1, 2, 3] -- [2, 4, 6]
print $ fn <$> Nothing -- Nothing
print $ fn <$> (Just 5) -- Just 10
print $ fn <$> (2, 3) -- (4, 6)
Applicative 型クラス
Applicative 型クラスは Functor 型クラスの派生クラスで pure
と <*>
を持つ。
main = do
print $ pure (*2) <*> Just 5 -- Just10
print $ pure (*2) <*> [1, 2, 3] -- [2, 4, 6]
main = do
print $ [(*2), (*3)] <*> [1, 2, 3] -- [2, 4, 6, 3, 6, 9]
利点がよく分からない。
Monad 型クラス
Monad 型クラスは Applicative 型クラスの派生クラスで return
と >>=
を持つ。
fn x = return (2 * x)
main = do
print $ [1, 2, 3] >>= fn -- [2, 4, 6]
print $ Just 5 >>= fn -- Just 10
print $ Nothing >>= fn -- Nothing
モジュール
ファイル名はモジュール名と同じでなければならないようだ。
-- MyModule.hs
module MyModule where
add x y = x + y
import MyModule
main = do
print $ add 3 5
高階関数
fn x = x * 2
ans = map fn [1, 2, 3]
main = print ans -- [2, 4, 6]
特に感想はなし。
部分適用
-- 部分適用を使用しない例
tax :: Double -> Double -> Double
tax rate price = rate * price
main = do
print $ tax 0.1 2500 -- 2500円の消費税(250円)を求める
print $ tax 0.1 3500 -- 3500円の消費税(350円)を求める
-- 部分適用を使用した例
tax :: Double -> Double -> Double
tax rate price = rate * price
jptax = tax 0.1 -- 部分適用を利用した関数を定義する(第二引数が省略されている)
main = do
print $ jptax 2500 -- 2500円の消費税(250円)を求める
print $ jptax 3500 -- 3500円の消費税(350円)を求める
ずっと気になっていた点だが、お行儀が悪いのか利点があるのか、果たして?
カリー化
関数
遅延評価
Haskell は遅延評価 (laze evaluation) する。多くの言語は正格評価 (strict evaluation) する。
-- 正格評価の場合:main = fn 3 7 11 が実行される
-- 遅延評価の場合:main = do { print (1+2); print (3+4) } が実行される。(5+6)は評価されない
fn x y z = do { print x; print y }
main = fn (1+2) (3+4) (5+6)
正格評価したいときはどうするんだろう。
とほほの Haskell 入門読了。図書館から借りてきた「Haskell 入門 関数型プログラミング言語の基礎と実践, 本間・類地・逢坂, 2017」を読むことにする。
p. 8
このように、遅延評価では評価の順番が予測困難なので、そのままでは書いた順に評価されてほしいI/Oとの相性がよくありません。これを解決するために、Haskell では IO モナドによって I/O 処理を直列化することで、I/O の発生順を制御します。
...このような巨大の未評価の式が溜まってメモリを消費することを、スペースリークと呼びます。
なるほど。メモリは空間計算量だからスペースリークはメモリリークと実質同じ?
p. 29
リストによる文字列の実装は、リスト用のすべての関数を再利用できるという点で優れています。しかし、Haskell のリストは連結リストであり、速度とメモリ使用量の両面から見て決して効率がいいものではありません。そのため、Haskell でテキストファイル解析や HTML の生成など大きな文字列を扱う場合は、
ByteString
型やText
型を使う必要があります。
なるほど。懸念していた配列はあるのだろうかという問題に対する一つの答えがこれというわけか。Haskell で良い実装をするのは知識とテクニックが必要そうで難しそうだ。
p.30
Unit 型は値を1つだけ持つ型です。型も値も
()
で表します。
Rust を勉強しているときにも思ったのだが、これはどうなのだろうか。型も値も同一視して同じ記法をするのは記号の濫用で良くないように思ってしまう。
p. 31
*16 Haskell でも
base
パッケージのDate.Void
モジュールにVoid
型が定義されています。しかし、これは関数の戻り値の形にできないなど他言語のvoid
や Unit 型とは異なるものです。Void
は空集合を表す型であり、pipes
パッケージなどで型変数に代入して使われます。また、さらに別のものとして、Control.Monad
モジュールにはvoid
関数があり、I/O アクションの戻り値を明示的に捨てるために使われます。
詳しくそれぞれのケースについて学ばないとよく分からないなぁ。
p. 33 ボトムについて。正しく評価できない式をボトムという。評価中にエラーになるものや、無限ループにより計算が終わらないものなどがその例である。文書中では ⊥ で表されることが多い。Haskell では x = x
や Prelude
モジュールの undefined
や error
がボトムの例である。ボトムは評価されるとエラーになる。逆に言えば、評価されなければ現れてもエラーにはならない。ボトムは任意の型を持てる。ボトムによって発生するエラーはコンパイルの型によるチェックでは見つけられないため安易に使いまくってはいけない。
p.34-38, 変数の宣言方法について。変数が宣言できる場所は
- トップレベル
where
-
let ... in ...
式
である。やっと let ... in ...
の意味が説明された。let
の後ろに変数宣言を置き、in
の後ろに式を置くとのこと。
p. 36. Haskell ではインデントにはスペースを使う。他の文字でも使えるものがあるらしいが warning 出るとのこと。
p. 40. 演算子の左右結合について。a ∘ b ∘ c
について (a ∘ b) ∘ c
と解釈されるとき ∘
を左結合の演算子といい、a ∘ (b ∘ c)
と解釈されるとき ∘
を右結合の演算子という。
p. 43.
Haskell では常に返り値は1つなのでタプルで複数個の返り値を表します。
タプルも有効利用するらしい。
p.44.
引数を明示した定義は関数の1点1点に対して値を決めているのに対し、関数合成による定義は変数(ポイント)を使わないという意味でポイントフリースタイルと呼びます。
私はポイントフリースタイル好きだな。
直後にポイントフリースタイルによる可読性の悪化について説明されている。
例えば、
(. (3 +)) . (*) . (2 +)
は\x y -> (x + 2) * (y + 3)
をポイントフリースタイルで書いたものです。
Julia だったら、無理くり書いて
Base.:∘(f::Function, fs::Tuple{Function, Function}) = x -> f(first(fs)(x), last(fs)(x))
∘(*, ((x -> x + 2) ∘ first, (y -> y + 3) ∘ last))
または
Base.:+(f::Function, g) = x -> f(x) + g
Base.:*(f::Function, g::Function) = x -> f(x) * g(x)
((identity + 2) ∘ first) * ((identity + 3) ∘ last)
または
Base.:+(f::Function, g) = x -> f(x) + g
Base.:+(f, g::Function) = x -> f + g(x)
Base.:+(f::Function, g::Function) = x -> f(x) + g(x)
Base.:*(f::Function, g) = x -> f(x) * g
Base.:*(f, g::Function) = x -> f * g(x)
Base.:*(f::Function, g::Function) = x -> f(x) * g(x)
Base.getindex(f::Function, i) = x -> f.(x)[i]
const id = identity
(id + 2)[1] * (id + 3)[2]
もしくは
Base.:+(f::Function, g) = x -> f(x) + g
Base.:+(f, g::Function) = x -> x + g(x)
Base.:+(f::Function, g::Function) = x -> f(x) + g(x)
Base.:*(f::Function, g) = x -> f(x) * g
Base.:*(f, g::Function) = x -> f * g(x)
Base.:*(f::Function, g::Function) = x -> f(x) * g(x)
Base.getindex(f::Function, i) = x -> f.(x)[i]
const id = identity
(id[1] + 2) * (id[2] + 3)
という感じだろうか。ラムダ式 (x, y) -> (x + 2) * (y + 3)
の方が普通ではある。とはいえ、(id[1] + 2) * (id[2] + 3)
は 25 文字で (x, y) -> (x + 2) * (y + 3)
よりも分かりやすいかもしれない。(i[1] + 2) * (i[2] + 3)
だと 23 文字になる。
もっと推し進めると (1₁ + 2) * (1₂ + 3)
で 19 文字みたいな文法を考えることができそう。ちなみに Haskell の (. (3 +)) . (*) . (2 +)
は 23 文字で十分小さかった。
こっちの定義の方が Julia らしくて良さそうだ。
Base.:+(f::Function, g) = (x...) -> f(x...) + g
Base.:+(f, g::Function) = (x...) -> f + g(x...)
Base.:+(f::Function, g::Function) = (x...) -> f(x...) + g(x...)
Base.:*(f::Function, g) = (x...) -> f(x...) * g
Base.:*(f, g::Function) = (x...) -> f * g(x...)
Base.:*(f::Function, g::Function) = (x...) -> f(x...) * g(x...)
Base.getindex(f::Function, i) = (x...) -> f.(x)[i]
const id = identity
((id[1] + 2) * (id[2] + 3))(1, 2) # 15
そろそろ Haskell の勉強に戻ろう。p. 45. 正格評価は先行評価ともいう。遅延評価は非正格評価ともいう。
p.48. 先行評価させる方法について。最初の方法は seq
を使う方法。他には $!
を使う方法がある。
p. 50. 中置演算子の部分適用を簡潔に記述するためのセクションという記法について。
-
(1 +)
で演算子+
の第一引数へ1
を部分適用した関数を表す。 -
(+ 1)
で演算子+
の第二引数へ1
を部分適用した関数を表す。
前置単項演算子と被るものについては第二引数に関するセクション記法は使えない。
通常の関数の部分適用では第一引数に関するものしか記述できない。
Haskell で単項演算子は -
しかないらしい。
p. 55. 純粋な関数内でデバッグのために出力をしたいときには GHC の提供する Debug.Trace
モジュールを使う。
p. 58. アズパターン (as pattern)。パターンマッチの際にパターンにマッチした値を変数に束縛したいとき、x'@(_, 0)
と 変数名@パターン
と書くと束縛できる。
p. 59. 反駁不可能パターン (irrefutable pattern, lazy pattern)。パターンマッチの際にマッチさせる値がボトムのとき、case 式全体の値もボトムになり、正格評価されてしまうのを防ぎたいとき、パターンの先頭に ~
を付けることで非正格にできる。これを反駁不可能パターンという。
ありがたさは、使ってみないと実感できなさそう。
p. 60. コンストラクターパターン。フィールドへのアクセサーとして使う。
x = (1, "one")
case x of (x1, x2) -> print x2
オブジェクト指向の魂みたいな機能。
p. 61. ガード節。
-- 「パターン | Bool 型の式」で 条件分岐ができる
f x = case x of n | n `mod` 2 == 0 -> putStrLn "even"
| otherwise -> putStrLn "odd"
-- 1つのパターンに複数のガード節を付けられる
g x = case x of (x1, True) | x1 `mod` 2 == 0 -> x1 `div` 2
(x1, _) | x1 `mod` 2 == 0 -> x1
| otherwise -> x1 - 1
-- <- でガード節にさらにパターンを書ける
h x = case x of (x1, True) | (q, 0) <- x1 `divMod` 2 -> q
(x_1, _) -> x1
p. 62. case 式以外でのパターンマッチ。
関数定義におけるパターンマッチ
パターンが複数ある場合は一気に定義しなければならない。
f (x1, True) | (q, 0) <- x1 `divMod` 2 = q
f (x1, _) = x1
g n | n `mod` 2 == 0 = putStrLn "even"
| otherwise = putStrLn "odd"
p. 63. 変数定義におけるパターンマッチ
変数定義でパターンを指定すると反駁不可能パターンとして扱われる。
(x, y) = 22 `divMod` 5
p. 64. リスト
[n,m..l]
は n
から l
までの数字を m - n
感覚で含むリストを表す。
つまり Haskell で [1,3..10]
は Julia なら 1:2:10
のことで、[n,m..l]
は n:m-n:l
のことである。
:
をパターンとして使うとリストを分割束縛できる。
x:xs = [1,2,3]
x -- 1
xs -- [2,3]
p. 65.
map (2 *) [1..10] -- [2,4,6,8,10,12,14,16,18,20]
filter (\n -> n `mod` 3 == 0) [1..10] -- [3,6,9]
foldl (-) 0 [1..10] -- -55
foldr (-) 0 [1..10] -- -5
p. 67. Maybe
percentage k n | n == 0 = Nothing :: Maybe Double
| otherwise = Just (100.0 * k / n)
-- "40.0"
case percentage 20 50 of Nothing -> "UNKNOWN"
Just x -> show x
Data.Maybe
モジュールを使うともっと便利になる。
import Data.Maybe
p = percentage 20 50
if isNothing p then "UNKNOWN" else show (fromJust p) -- "40.0"
import Data.Maybe
maybe "UNKNOWN" show (percentage 20 50)
p.68 Either
Either
型はエラーが発生するかもしれない処理の返り値として使われます。Either
型の値を作るコンストラクタは2つあり、正常時の値はRight
、異常時の値はLeft
で包んで表現します。
なるほど。Right
と Left
の謎が解けた。ということは Julia で言うところの単なる Union{A, B}
とは意味が違う。Union{SomeError, A}
というところか。
Data.Either
に Either
型を操作する関数が定義されている。
p.68-71. ループ
- 再帰
map
filter
foldl
foldr
-
Control.Monad
のforM_
module Main (main) where
import Control.Monad (forM_)
main :: IO ()
main = do
forM_ [1..20] $ \i -> putStrLn (fizzbuzz i)
where
fizzbuzz n
| n `mod` 15 == 0 = "fizz buzz"
| n `mod` 3 == 0 = "fizz"
| n `mod` 5 == 0 = "buzz"
| otherwise = show n
などを使う。
fizz buzz の例とともに習っていない when
が出てきている。ググったら Control.Monad
で定義されていて、
-
when A B
でA
が真のときB
をする ⇔ Julia のA && B
-
unless A B
でA
が偽のときB
をする ⇔ Julia のA || B
ということのようだ。入門書なら習っていない制御文が出てくるのはやめて欲しい。
p. 71-80. モジュールとパッケージ
Haskell にはプログラムを複数のファイルに分割して書くための単位として、モジュールとパッケージがあります。GHC においては、モジュールはファイル単位でのプログラム分割、パッケージは複数のモジュールをまとめたライブラリの単位でのプログラム分割を担います。
出た!Python とか Java と同じでこれは悪いモジュール機能だ。ファイル単位とモジュール単位が結びついていて分離できないインターフェイスだ。ファイル単位でモジュール分割するのはやめて欲しい。プログラムとしての姿をディレクトリー・ファイルの構造と極力結び付けないで欲しい。
GHC においては、モジュール名とファイル名は一対一で対応します。モジュール名は大文字から始まり、2文字目以降は変数名と同様 Unicode まで含めた英数字と
'
が続きます。そして.
で区切ることで階層化できます。MyApp.MyModel.MyUtil
のようなものをモジュール名として使えます。
んーと、日本語とかも使えるのかな?でも変数名のところには使えないって書いてあったし多分使えないのだろう。ここの説明は微妙だな。Go もそうだが、変数名にきつい制約があるのはやめて欲しい。自由な言語としてどうなんだ。
パッケージは Cabal 形式で作成する。レポジトリには Hackage と Stackage がある。この本は Stackage の使用を推奨している。パッケージのバージョニングはドット区切りの4桁の数字?なのかな。セマンティックバージョニングとは異なるようだ。
Haddock というソースコードにコメントでドキュメントを書く形式があるようだ。
第1章のはじめての Haskell と第2章の基本の文法までを読了した。
p. 81-87. 第3章の型・型クラス。
3.1 型の記述。式 :: 型
と書く。3.2 型システム。3.2.1 型チェック。3.2.2 多相性。
一つの式に汎用の型を表す型変数を割り当て、複数の具体的な型で利用できるようにした型システムはパラメータ多相 (parametric polymorphism) を持つといいます。
パラメーター多相性の説明。
Haskell では型クラス (type class) という仕組みを利用して、方によって全く異なる実装の式を使えます。
Haskell で出てくる用語「型クラス」の登場だ。
型クラスによる多相は、必要に応じて型ごとに場当たり的に違う実装を追加できるため、アドホック多相 (ad-hoc polymorphism) と呼ばれます。アドホック多相は、オブジェクト指向言語におけるオーバーロードに相当します。
アドホック多相性の説明。
3.3 型コンストラクタと型変数
今まで単純に型と呼んできた型の識別子は、正確には型コンストラクタと呼びます。型コンストラクタは単体で使う他、型引数という引数をとるものもあります。
ふむ。型コンストラクタとな。型引数の話はパラメーター型 (parametric type) 的な?
IO
やMaybe
はカインドが*
でないので正確には型ではありません
次のページにカインドの説明が書いてあるので読む。
p. 88-89. カインド。
型を組み合わせる際のルールを規定するために使うものをカインド (kind) と呼びます。
カインドは基本的には*
(star) と->
だけで表され、型コンストラクタと型引数の組み合わせを規定します。
Int
や String
のカインドは *
である。IO
のカインドは * -> *
である。
*
のカインドを持つ型コンストラクタや、型コンストラクタの組み合わせは一般に型と呼ばれます。
正確には型ではありませんの話は分かった。
p. 90. 3.3.2 型変数。
型の変数を型変数 (type variable) という。慣習的に小文字一文字を使うことが多い。型変数を特定の型クラスに所属するものに制約できる。これを型制約 (type constraint) という。これは 式 :: 型クラス 型変数 => 型
と書く。
show :: Show a => a -> String
p. 91. 3.4 代数的データ型。
代数的データ型 (algebraic data type, ADT)
- データコンストラクタ
data 型コンストラクタ名 = データコンストラクタ名 フィールドの型1 フィールドの型2 ...
- 型コンストラクタ名とデータコンストラクタ名は同一のものを使うことが多い。
- upper camel case が慣習のようだ。
- データコンストラクタは単にコンストラクタと呼ばれることが多い。
- 複数のデータコンストラクタからの型コンストラクタの定義
data 型コンストラクタ名 = データコンストラクタ名1 フィールドの型1-1 フィールドの型1-2 ... | データコンストラクタ名2 フィールドの型2-1 フィールドの型 2-2 ...
p. 94. 正格性フラグ。
デフォルトだとコンストラクタはすべての引数について非正格だが、コンストラクタの定義において型の前に正格性フラグ !
を付けると、その引数について正格になる。
p. 96. レコード記法。
data 型コンストラクタ名 = データコンストラクタ名 { フィールド名1 :: 型名1, フィールド名2 :: 型名2 }
呼び出しは データコンストラクタ名 { フィールド名1 = 値1, フィールド名2 = 値2 }
。
data Employee = NewEmployee { employeeAge :: Integer, employeeIsManager :: Bool, employeeName :: String } deriving (Show)
employee :: Employee
employee = NewEmployee { employeeName = "Subhash Khot", employeeAge = 39, employeeIsManager = False }
main :: IO ()
main = do
putStrLn $ show $ employeeAge employee
putStrLn $ show $ employeeIsManager employee
putStrLn $ show $ employeeName employee
フィールド名は名前空間を汚すらしい。
p. 103. 型の別名
型名が長過ぎて可読性を損ねていたり、同じ形式であってもデータの保つ意味が違ったりする場合、型に別の名前を付けられると便利です。
前者の目的ではtype
キーワードを、後者の目的ではnewtype
キーワードを使います。
なるほど。
p. 107.
newtype
は型が区別される以外はtype
と同等であるため、値の変換コストがまったくないことが保証されています。
type
と newtype
をどういうときに使うのかは分かった気がする。
type 新しい型名 = 元にする型名
newtype 型コンストラクタ名 = データコンストラクタ名 型
p.107. 型クラス。
型クラスに所属する型を、その型クラスのインスタンス (instance) という。ひとまず、型クラスは型制約に使える、複数の型をまとめるためのカテゴリーだと思えばいいらしい。そうだとやはり C++ や Nim のコンセプトや、Julia の抽象型みたいなもんか?Rust のトレイト的な?
p. 108. 型クラスで定義され、任意の型によって具体的に実装される、型クラスに紐付いた関数をメソッドという。
Java と Haskell の用語の対応は以下の感じらしい。
Java | Haskell |
---|---|
インタフェース | 型クラス |
具象クラス | インスタンス |
インスタンス | 値 |
まあ私の認識はそんなに間違っていなさそうだ。
p. 110-112. 型クラスの定義とインスタンスの定義。
data Dog = Dog deriving Show
data Cat = Cat deriving Show
data Human = Human String deriving Show
class Greeting a where
name :: a -> String
hello :: a -> String
hello _ = "..."
bye :: a -> String
bye _ = "..."
instance Greeting Human where
name (Human n) = n
hello h = "Hi, I'm " ++ name h ++ "."
bye _ = "See you."
instance Greeting Dog where
name _ = "a dog"
hello _ = "Bark!"
instance Greeting Cat where
name _ = "a cat"
bye _ = "Meow..."
main :: IO ()
main = do
putStrLn $ hello $ Human "takeshi"
putStrLn $ hello Dog
putStrLn $ hello Cat
まあこれはいいだろう。不思議なところはない。
p. 113. Monoid 型クラス。
import Data.Monoid
main :: IO ()
main = do
print $ [1,2,3] <> [4,5] -- [1,2,3,4,5]
print $ "AB" <> "C" <> "DE" -- "ABCDE"
print $ getSum $ 3 <> 2 -- 5
print $ getProduct $ 3 <> 2 -- 6
let maybes = [Nothing, Just 1, Just 100, Nothing]
print $ getFirst $ mconcat $ map First maybes -- Just 1
print $ getLast $ mconcat $ map Last maybes -- Just 100
let f = appEndo $ Endo (subtract 1) <> Endo (^ 2)
print $ f 3 -- 8
[Int]
は ++
に関してモノイド。String
は ++
に関してモノイド。Int
は +
に関してモノイド。Int
は *
に関してモノイド。Maybe a
は一番左の nothing でない値を取ってくる操作に関してモノイド。一番右の nothing でない値を取ってくる操作に関してもモノイド。 Int -> Int
は .
に関してモノイド。
代数学の初歩が分からない人には分からなさそう。
p. 115-117. 型制約。
型クラスは Constraint
カインドを持つ。
(型クラス1 型変数1, 型クラス2, 型変数2) => 型変数1 型変数2 ...
と書く。
import Data.List (intercalate)
data Dog = Dog deriving Show
data Cat = Cat deriving Show
data Human = Human String deriving Show
class Greeting a where
name :: a -> String
hello :: a -> String
hello _ = "..."
bye :: a -> String
bye _ = "..."
instance Greeting Human where
name (Human n) = n
hello h = "Hi, I'm " ++ name h ++ "."
bye _ = "See you."
instance Greeting Dog where
name _ = "a dog"
hello _ = "Bark!"
instance Greeting Cat where
name _ = "a cat"
bye _ = "Meow..."
sayHello :: Greeting a => a -> IO ()
sayHello x = putStrLn $ hello x
class Greeting a => Laughing a where
laugh :: a -> String
instance Laughing Human where
laugh _ = "Zehahahah...!!"
leaveWithLaugh :: Laughing a => a -> IO ()
leaveWithLaugh x = do
putStrLn $ bye x
putStrLn $ laugh x
liftGreet :: (a -> String) -> ([a] -> String)
liftGreet f = intercalate "\n" . map f
instance Greeting a => Greeting [a] where
name = liftGreet name
hello = liftGreet hello
bye = liftGreet bye
main :: IO ()
main = do
sayHello $ Human "takashi"
leaveWithLaugh $ Human "takashi"
sayHello [Human "atsuhiko", Human "shingo"]
Hi, I'm takashi.
See you.
Zehahahah...!!
Hi, I'm atsuhiko.
Hi, I'm shingo.
class 宣言における型制約まではそんなに難しくなかったが、instance 宣言における型制約の例はなかなか見慣れなくて私にはまだ難しい。これが関数型プログラミングかという感じ。しばらくやったら慣れるかな?やっぱ自然と部分適用が駆使されていることの認知負荷が高い。
p. 117-119. 型変数の曖昧性。
hello
とbye
の例は、liftGreet
の第一引数を Java でいうthis
であると考えるとオブジェクト指向に近い型クラスの使い方と言えます。
うーん、しっくりこないので Java で解釈して書いてみようかなあと思ったがやめた。
Haskell の型クラスでは、型変数を利用さえしていれば型変数の使われている位置に関わらず、どんな関数でもオーバーロード可能にします。
Julia みたいな感じ?でも Julia の型システムと同じやつは Common Lisp Object System だけだって聞いたし多分違うんだろう。Haskell と Julia の双方に詳しい人に訊きたいところ。
data Human = Human String deriving Show
class Greeting a where
name :: a -> String
hello :: a -> String
hello _ = "..."
bye :: a -> String
bye _ = "..."
instance Greeting Human where
name (Human n) = n
hello h = "Hi, I'm " ++ name h ++ "."
bye _ = "See you."
class Breeding a where
breed :: String -> a
instance Breeding Human where
breed = Human -- この Human はコンストラクタ
main :: IO ()
main = do
let baby = breed "takeshi"
putStrLn $ hello baby
$ ghc constraint.hs
[1 of 2] Compiling Main ( constraint.hs, constraint.o ) [Source file changed]
constraint.hs:23:14: error:
• Ambiguous type variable ‘a0’ arising from a use of ‘breed’
prevents the constraint ‘(Breeding a0)’ from being solved.
Relevant bindings include baby :: a0 (bound at constraint.hs:23:7)
Probable fix: use a type annotation to specify what ‘a0’ should be.
Potentially matching instance:
instance Breeding Human -- Defined at constraint.hs:18:10
• In the expression: breed "takeshi"
In an equation for ‘baby’: baby = breed "takeshi"
In the expression:
do let baby = breed "takeshi"
putStrLn $ hello baby
|
23 | let baby = breed "takeshi"
| ^^^^^
constraint.hs:24:14: error:
• Ambiguous type variable ‘a0’ arising from a use of ‘hello’
prevents the constraint ‘(Greeting a0)’ from being solved.
Relevant bindings include baby :: a0 (bound at constraint.hs:23:7)
Probable fix: use a type annotation to specify what ‘a0’ should be.
Potentially matching instance:
instance Greeting Human -- Defined at constraint.hs:10:10
• In the second argument of ‘($)’, namely ‘hello baby’
In a stmt of a 'do' block: putStrLn $ hello baby
In the expression:
do let baby = breed "takeshi"
putStrLn $ hello baby
|
24 | putStrLn $ hello baby
|
以下のように型注釈を入れるとコンパイルできて動く。
hello (baby :: Human)
うーん、理解できない。Human
と他に何と曖昧になるのかが分からない。
一般過ぎる型の例。
data Human = Human String deriving Show
class Greeting a where
name :: a -> String
hello :: a -> String
hello _ = "..."
bye :: a -> String
bye _ = "..."
instance Greeting Human where
name (Human n) = n
hello h = "Hi, I'm " ++ name h ++ "."
bye _ = "See you."
class Breeding a where
breed :: String -> a
instance Breeding Human where
breed = Human -- この Human はコンストラクタ
clone = breed . name
main :: IO ()
main = do
let baby = breed "takeshi"
putStrLn $ hello (baby :: Human)
putStrLn $ hello $ clone (Human "takeshi")
$ ghc constraint.hs
[1 of 2] Compiling Main ( constraint.hs, constraint.o ) [Source file changed]
constraint.hs:21:9: error:
• Ambiguous type variable ‘c0’ arising from a use of ‘breed’
prevents the constraint ‘(Breeding c0)’ from being solved.
Relevant bindings include
clone :: Human -> c0 (bound at constraint.hs:21:1)
Probable fix: use a type annotation to specify what ‘c0’ should be.
Potentially matching instance:
instance Breeding Human -- Defined at constraint.hs:18:10
• In the first argument of ‘(.)’, namely ‘breed’
In the expression: breed . name
In an equation for ‘clone’: clone = breed . name
|
21 | clone = breed . name
| ^^^^^
constraint.hs:27:14: error:
• Ambiguous type variable ‘c0’ arising from a use of ‘hello’
prevents the constraint ‘(Greeting c0)’ from being solved.
Probable fix: use a type annotation to specify what ‘c0’ should be.
Potentially matching instance:
instance Greeting Human -- Defined at constraint.hs:10:10
• In the first argument of ‘($)’, namely ‘hello’
In the second argument of ‘($)’, namely
‘hello $ clone (Human "takeshi")’
In a stmt of a 'do' block:
putStrLn $ hello $ clone (Human "takeshi")
|
27 | putStrLn $ hello $ clone (Human "takeshi")
|
let clone x = breed (name x) :: a
としたいが、Haskell では式中で型変数を参照できずこう書けない。asTypeOf
を使って解決する。
clone x = breed (name x) `asTypeOf` x
asTypeOf
は const
と似ているらしいが、const
なんて今まで出てこなかったのだから、何のことか分かるわけがない。分からないので飛ばす。
Haskell 2010 に従うと、String
型を型クラスのインスタンスにはできないらしい。インスタンス宣言に型シノニムを使えないらしい。しかし、[Char]
を使ってもインスタンスの定義はできないらしい。解決策は newtype
によって別の型を定義することらしい。定義できないようにしている理由は曖昧性が生じるのを防ぐためらしい。上記の曖昧性を理解できてから考えればいいや。
p. 122-129. Prelude における型クラス。
-
Show
型クラス-
show
メソッド: データを文字列にする -
print
関数:print = putStrLn . show
-
-
Read
型クラス-
read
関数: 文字列をデータ型にする
-
-
Eq
型クラス-
==
メソッド: 等値性 -
/=
メソッド: 非等値性
-
-
Ord
型クラス<
<=
max
min
compare
-
Ordering
型LT
EQ
GT
-
Enum
型クラス-
fromEnum
メソッド:Enum
型クラスのインスタンスからInt
型の値へ変換する -
toEnum
:Int
型の値をEnum
型クラスのインスタンスへ変換する
-
-
Num
型クラス-
+
メソッド -
-
メソッド -
*
メソッド -
Fractoinal
型クラス-
/
メソッド -
Floating
型クラスsin
log
-
fromInteger
-
-
Real
型クラス -
RealFrac
型クラス -
RealFloat
型クラス -
Integral
型クラスtoInteger
-
Integer
型 -
Rational
型fromRational
toRational
なんか数値型はごにょごにょ書いてあるが、よく分からないので必要になったら覚えよう。
p. 130. deriving によるインスタンス定義。
deriving
はコンパイラがサポートしている特定の型クラスに対して、自動的にインスタンス定義を作ってくれる機能です。
deriving
で指定できる型クラスはPrelude
モジュールのEq
、Ord
、Enum
、Bounded
、Show
、Read
、そしてData.Ix
モジュールのIx
です。計7つしかありません。
えー?ユーザーが自由に derive できないらしい。
第3章読了。型変数の曖昧性のところだけしっくりこなかった。
疲れてきたのでHaskellでの型レベルプログラミングをぱらぱらと見る。
古典的なHaskell(Haskell 2010)ではカインドは * と -> からなるものだけですが、現代のGHCではそれ以外のカインドも使用できます(実際のところ、DataKinds拡張により任意のデータ型をカインドとして用いることができます)。詳しくは後述します。
Haskell 2010 は古典的な Haskell らしい。GHC 拡張に踏み込みすぎないほうが良いのかと思ったがそんなことはないようだ。
GHCi を使ってなかったが便利そうなので使おうか。
第4章 I/O 処理も大事な章だと思うがぱらぱらと読んだ。後で詳しくやろう。第5章のモナドに進む。
p. 163-164. モナド。
Haskell では
Monad
型クラスのインスタンスでモナド則という一定の規則を満たしている型をモナドと呼びます。
モナドはその「計算」に対して手続き的にプログラムを組み上げる方法を提供します。
私は圏論は理解できていない。
p. 164. 5.1 モナドアクション。
Monad
型クラスのインスタンスを GHCi で確認する。
ghci> :i Monad
type Monad :: (* -> *) -> Constraint
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
{-# MINIMAL (>>=) #-}
-- Defined in ‘GHC.Base’
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b) => Monad ((,,) a b)
-- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b, Monoid c) => Monad ((,,,) a b c)
-- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Monad Solo -- Defined in ‘GHC.Base’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad (Either e) -- Defined in ‘Data.Either’
p. 166.
Haskell プログラマは、
Monad
インスタンスの値をたびたびモナドアクション、あるいは単にアクションと呼びます。
このように、
Monad
型クラスは、インスタンスの型を do 式で手続き的に書けるようにします。
特定の処理を手続き的に書けるようにする、Haskell におけるモナドが提供する機能はそれだけです。それ以上でもそれ以下でもありません。
それなら話はそんなに難しくなさそうだなぁ。
やっぱり第4章のI/O処理をやる。
p. 131-137.
I/Oアクションの組み立て。
(>>=) :: Monad m => m a -> (a -> m b) -> m b
return :: Monad m => a -> m a
IO
で置き換えて考えると
(>>=) :: IO a -> (a -> IO b) -> IO b
return :: a -> IO a
つまり、return
は与えられた値を I/O アクションとして返す関数。使用例。
main = return ()
main = getContents >>= putStr
echo "Hello!" | ./main
>>
は >>=
の値を捨てる版。
p.138 コマンドライン引数。
import System.Environment
main = do
args <- getArgs
print args
p. 138-139. 環境変数。
System.Environment
の
getEnv :: String -> IO String
lookupEnv :: String -> IO (Maybe String)
setEnv
unsetEnv
を使う。
p. 139-149. 入出力。
基本操作は
putChar
putStr
putStrLn
getChar
getLine
readLn
writeFile
readFile
appendFile
getContents
など。type FilePath = String
である。
これらの関数はいずれも Handle
で動いている。
hPutStrLn
hGetLine
hGetContents
openFile
hClose
hIsEOF
hSetBuffering
hGetBuffering
バイナリーファイルは bytestring
パッケージの Data.ByteString
モジュールの ByteString
を使う。
System.IO
モジュールで提供されている。
p. 149-154. ファイルシステム。
p. 154-162. 例外処理。
基本は Maybe
や Either
を使うのがお上品。
base
パッケージの Control.Exception
モジュールで提供されている関数群を使う。
catch :: Exception e => IO a -> (e -> IO a) -> IO a
二項演算子として使われることが多い。
finally :: IO a -> IO b -> IO a
catch
と合わせて使う。
全ての例外を区別せずに catch
するなら SomeException
を使う。
displayException
でエラーを表示できる。
import Control.Exception
main =
(readFile "dummyFileName" >>= putStrLn)
`catch`
(\(e :: SomeException) ->
putStrLn $ "readFile failure!!! : " ++ displayException e)
`finally`
(putStrLn "finalization!!!")
例外を正格評価させたいときとかは、$!
とか deepseq
パッケージの Control.Deepseq
を使う。