Open119

Haskell 学習記

PaalonPaalon

なんでも知っているフリをするには Haskell を知らないというワケにはいかない気がするので Haskell を学ぶ。ツッコミどころがあれば誰でも何でも大歓迎。お気軽にどうぞ。

PaalonPaalon

まずは環境構築。私の環境は macOS + Intel + Fish である。haskell install でググったらトップに出てきた記事 Haskellの環境構築2023 によると、GHCup の指示通りに従うのが良さそうなのでそうする。

PaalonPaalon

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/

いくつか質問を聞かれるので、答えておく。読めば分かる親切な構成になっていて良い。

PaalonPaalon

アンインストールしたければ

  • ~/.ghcup を削除する
  • ~/.config/fish/config.fish に追加された # ghcup-env のコメントを持つ一行を消す(私は Fish なのでこれだが他のシェルなら適当な他の設定ファイル)

をすれば良いだろう。

PaalonPaalon

シェルを再起動して以下のファイルを作って

-- 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!

正常にインストールはできたようだ。

PaalonPaalon

それでは教材を選ぶ。ちなみにハローワールドと 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/ から始めることにする。

PaalonPaalon

6 ページ目。

最初は上記のハローワールドの説明。

  • :: は "type of" の意味。
  • = は等値の意味。
  • putStrLn の型は String -> IO ()
  • main の型は IO ()

なんでも型がつく静的言語というところだろうか。わかりやすくて良い。

PaalonPaalon

7 ページ目。

  • IO a はこれはサブルーチン(実行されるとき、入出力アクションを実行するかもしれず、最後に型 a の値を返す)の記述である。
  • main はプログラムのエントリーポイントの名前である。
  • Haskell runtime は main を見つけて実行する。

C/C++ だったら main 関数の返り値の型を int とだけ記述したり、Go や Rust の main では何も指定しないのとは対照的である。

PaalonPaalon

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!

入出力の例だな。

PaalonPaalon

9 ページ目。

  • Haskell は indentation sensitive である。
  • 何らかの表現の一部のコードはその表現の始まりよりもインデントされていなければいけない。

インデント数はなんでもいいのだろうか?

PaalonPaalon

10 ページ目。

  • do は特別な文法(sequence 入出力アクションをさせてくれる)である。
  • getLine の型は IO String である。
  • IO String の意味は…(以下略)
  • getLine は標準入力から一行を取り出して String 型の値を生成する。

関数の呼び出しには括弧はいらないということだろう。今までやってきた言語との一番の違いはここな気がする。
... <- ...... = ...let ... = ... の違いがまだ分かっていない。次のページに説明があるだろう。進もう。

PaalonPaalon

11 ページ目。

  • getLine の型は IO String である。
  • name の型は String である。
  • <- は特別な文法(do 記法の中でのみ現れる)である。
  • <- は「サブルーチンを実行し、その結果得られた値を <- の左側にある名前に結び束ねる」ということを意味する。
  • let <name> = <expr> は「残りの do ブロックにおいて、<name><expr> と相互に交換可能である」ということを意味する。
  • do 記法中では、letin とともに使われなくても良い。

副作用のある処理の結果を代入するときには <- を使えば良いのだろうか?単なる =let 付きの = の違いがよく分からない。最後の letin の話は何のことを言っているのか分からない。だってまだ in が出てきていないからね。<-= の書き間違いにはしばらく苦しまされそうだ。

PaalonPaalon

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 の配列型という意味だろう。

PaalonPaalon

13 ページ目。よくあるエラーその1:String の代わりに IO String を使ってしまうこと。

  • 註: Stringtype String = [Char] と定義されている。
  • Haskell は期待される型 StringgetLine の型 IO String が一致させられないと言っている。
  • IO aa は違う型である。

分かる。

PaalonPaalon

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. 1つ目のエラーは型 [] を型 IO に合わせられないというエラー。IO () を期待したが [()] が来た、と。
  2. 2つ目のエラーは型 Char を型 () に合わせられないというエラー。[()] を期待したが String が来た、と。
  3. 3つ目のエラーは型 Char を型 () に合わせられないというエラー。[()] を期待したが String が来た、と。

とりあえず型 () は C/C++ でいうところの void、Julia でいうところの nothing、 Python でいうところの None、 JavaScript でいうところの undefined だな。
1つ目のエラーは何も返さない IO を期待したが、() の配列が来たということか。

関係ないけど In an equation for ‘main’: って書いてあるから main = do ... の表現は等式 (equation) というようだ。

分からないので2つ目と3つ目のエラーを観察する。nameString なのは私の期待通りであるが、Haskell の期待通りではないらしい。Haskell は [()] を期待しているらしい。うーん分からない。++ の仕様が分かっていないせいだろうか。

答えを見ずに悩むのはこれくらいにして、次のページに進もう。

PaalonPaalon

15 ページ目。よくあるエラーその2。関数適用は演算子適用に先行する。(Common Error #2 - Function application precedes operator application)

フランス語だと関数のことを application と言うのでフランス人が見たら発狂しそうな一文である。

  • 文字列表現の周りに丸括弧が必要である。
putStrLn ("Nice to meet you, " ++ name ++ "!")

なるほど。括弧は省略できるだけでないわけではないようだ。それでも省略できるときは省略したがるスタイルなんだろう。関数名と括弧の間にスペースを入れるのはこだわりなのかな?他の言語じゃかなり嫌われるので。

上記の通りに括弧を入れると正しく動作した。関数と括弧の間のスペースを消して、コンパイル&実行してみても何も文句を言われずに動作したので、消しても問題はないようだ。

PaalonPaalon

16 から 21 ページ。TODO アプリの設計について。

type Item = String
type Items = [Item]
PaalonPaalon

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\mathrm{addItem}: \mathrm{Item} \times \mathrm{Items} \to \mathrm{Items} という意味だろう。displayItems はそのままの意味で、removeItem :: Int -> Items -> Either String Items\mathrm{removeItem}: \mathrm{Int} \times \mathrm{Items} \to \mathrm{String} \cup \mathrm{Items} という意味だろう。ということからすると、-> は左から優先的に結合することになっているのだろう。デカルト積を表す記号がないとヤバそうだが、大丈夫だろうか?

PaalonPaalon

23 ページ目。addItem。

-- Returns a new list of Items with the new item in it
addItem :: Item -> Items -> Items
addItem item items = item : items

a : bab を結合した配列を表すようだ。

PaalonPaalon

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 を使って zipWithreverseunlines を検索して詳しく調べよ。
  • 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 も調べたが想像通りの意味だった。あとは letin の使い方だけ分からない。評価についてのガイドは今は読まなくてもいいだろう。次のページに進もう。

PaalonPaalon

25 ページ目。User Interaction。

letin についての説明が来るかと思ったら来なかった。まあ後で調べることにする。

  • removeItem は今は飛ばして、ユーザーインタラクションの機能を加えよう。
-- Takes a list of items
-- Interact with the user
-- Return an updated list of items
interactWithUser :: Items -> IO Items
PaalonPaalon

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 なしを常に意識しなければならず、認知コストが上がりそうだ。

PaalonPaalon

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."

今までのを繋ぎ合わせる。詳細な例は書いていないが多分こういうことだろう。コンパイルして実行したら動いた。想定読者がよく分かっていないけれど、プログラミング初心者には不親切だろうな。

PaalonPaalon

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

コードを上記の通り編集して実行して期待の動作を確認。

PaalonPaalon

32 ページから 33 ページ。

data Command
  = Quit
  | DisplayItems
  | AddItem String
  • 新しい ADT を作成して、ユーザーコマンドの可能性をモデル化する。

ADT が何のイニシャリズムか分からなかったが、調べたら algebraic data type のイニシャリズムのようだ。イントロダクションならそのくらい、略さないで言ってもらいたいという愚痴。プログラムの中身は分かった。

PaalonPaalon

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 のところはよく分からない。RightLeft も何をしているのか分からない。次に進もう。

PaalonPaalon

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

RightLeft 以外のところ以外は不思議なところはないかな。次に進もう。

36 ページ目。実行できた。RightLeft の説明はしてくれないようだ。後で調べよう。

PaalonPaalon

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
    ...

特に言及することはなさそう。

PaalonPaalon

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 が出てきた。何をしているのかよくわからない。どれが関数でどれが値なのか分からないという感じ。飛ばそう。次に進む。

PaalonPaalon

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 とかも分かっていないか。次に進もう。

PaalonPaalon

40 ページから 43 ページでおしまい。

最後らへんで急に説明が何もなくなってしまった。入門資料としてはあまりよくなかったかもしれない。まあでも、分からんポイントが出てきたので良しとしよう。次はどの資料を見ようか。

PaalonPaalon

いろんな入門を書いているとほほのHaskell入門 を見ることにする。
分かっているところは飛ばしてカラカラと見る。
予約語をそういえば見ていなかった。

case		class		data		default		deriving
do		else		foreign		if		import
in		infix		infixl		infixr		instance
let		module		newtype		of		then
type		where		_

ふむふむ。

PaalonPaalon
  • インラインコメントは -- で始める。
  • ブロックコメントは {- ブロックコメント -} である。
  • { 式1; 式2; 式3 } として複数の式を1つの式にできる。
  • インデントと改行でブロックの波括弧とセミコロンを省略できる。
PaalonPaalon

主な型

  • 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 は任意の型だったらしい。衝撃。

PaalonPaalon

変数名には英数字とアンダーバーとシングルクォートが使える。小文字始まりでなければならない。
リストはリストであってシリアライズされたデータである配列にはなっていないらしい。配列はあるんだろうか。

  • [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 で言えば catappend! 的な感じだろう。
  • 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 なら reversereverse!
  • map (*2) [1, 2, 3] リストに対して関数を適用する。Julia なら [1, 2, 3] * 2[1, 2, 3] .* 2map(x -> x * 2, [1, 2, 3]) など。

以下は等価。

  • [1, 2, 3]
  • 1:[2, 3]
  • 1:2:[3]
  • 1:2:3:[]
PaalonPaalon

Haskell の例

s = [x * x | x <- [1..5]] -- [1, 4, 9, 16, 25]

は Julia なら

s = [x * x for x = 1:5]
PaalonPaalon

Haskell の例

s = [x * x | x <- [1..5], x /= 3]

は Julia なら

s = [x * x for x = filter(x -> x  3, 1:5)]
PaalonPaalon

タプルはリストと似ていますが、要素は同じ型である必要はありません。

逆に言えば、リストは型が同じでなければならないようだ。これはかなりきつそう。

  • (1, 2, 3)
  • (1, 'a', "ABC")

要素数が 0個のタプルはユニット(unit)と呼ばれます。

func = return ()

ということは要素数が0個のタプルであるユニットを Julia でいうところの nothing として使っているということだろう。つまり Julia で言うところの ()nothing の区別はないということだろう。これもきつそう。

PaalonPaalon

タプルの要素の取り出し

fst (1, 'a', "ABC") -- 1
snd (1, 'a', "ABC") -- 'a'
(_, _, x) = (1, 'a', "ABC") -- binds x to "ABC"
PaalonPaalon

演算子

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) で囲むと二項演算子として使える。

PaalonPaalon

関数

add x y = x + y

上記の下で

main = print (add 3 5)
main = print $ add 3 5

は等価である。

PaalonPaalon

二項演算子定義

以下の記号からなる文字列として二項演算子を定義できる。

# $ % & * + . / < = > ? @ \ ^ | - ~
  • infix 結合なし
  • infixl 左結合
  • infixr 右結合

を使って定義した二項演算子の優先度を 0 から 9 までで指定できる。

PaalonPaalon

ラムダ式

\arg -> expr
\(arg1, arg2) -> expr

バックスラッシュはラムダ式の記法だったようだ。

PaalonPaalon

パターンマッチ

関数を引数の値によって別々に定義できる。

func 1 = "One"
func 2 = "Two"
func 3 = "Three"
main = print $ func 1

これは Julia にないし、Rust にもない?のかな。便利だと思う。

PaalonPaalon

ガード条件

foo x
  | x == 1 = "One"
  | x == 2 = "Two"
  | x == 3 = "Three"
  | otherwise = "More..."

main = putStrLn $ foo 2

これも Julia にないし、いい機能だと思う。

PaalonPaalon

関数合成

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

引数補足

関数の引数は @ を用いて複数の形式で受け取ることができます。下記では、引数の文字列全体を str として、また、先頭の文字を x、残りの文字を xs として受け取ることができます。

func str@(x:xs) = do
 print str		-- "ABCDE"
 print x		-- 'A'
 print xs		-- "BCDE"

main = do
  func "ABCDE"

ほぇ〜。便利かどうかは使って慣れてみないと分からないかな。

PaalonPaalon

do 文

do は式や指定した式を処理します。式には ブロック を指定することもできます。

main = do { print "A"; print "B"; print "C" }

ブロックは レイアウト を用いて下記の様に記述することもできます。

main = do
  print "A"
  print "B"
  print "C"

Scheme の begin みたいなものかと。

PaalonPaalon

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 の意味が分からない。他の説明を見ようかな。

PaalonPaalon

if 文 はまあいいだろう。
case 文。

case expr of
  pattern1 -> expr1
  pattern2 -> expr2
  _ -> expr3

case ... of ... -> ... ... -> ... _ -> ... ね。Julia にはないので(愚直に if ... elseif ... else ... を書く)たまに欲しくなる。

PaalonPaalon

where 文

where 文は、変数や補助関数を局所的に定義します。

main = print $ add x y
  where
    x = 123
    y = 456
    add x y = x + y

うーんと、where はなくても良いということだろうか?分からない。名前を散らかさないようにスコープを作るのに便利なのかな?

PaalonPaalon

import 文

import [qualified] ModuleName [as AliasName] [[hiding] (name, ...)]

使いながら覚えないとよく分からなさそう。モジュールがあることは良いことだ。

PaalonPaalon

ループ

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"
PaalonPaalon

データ型

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

というところか。

PaalonPaalon

新型定義

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 ということだろう。

PaalonPaalon

型クラス

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

あまり不思議なところはない。

PaalonPaalon

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 で在るらしい。

PaalonPaalon

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 <$> xfmap 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)
PaalonPaalon

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]

利点がよく分からない。

PaalonPaalon

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
PaalonPaalon

モジュール

ファイル名はモジュール名と同じでなければならないようだ。

-- MyModule.hs
module MyModule where

add x y = x + y
import MyModule

main = do
  print $ add 3 5
PaalonPaalon

高階関数

fn x = x * 2
ans = map fn [1, 2, 3]
main = print ans		-- [2, 4, 6]

特に感想はなし。

PaalonPaalon

部分適用

-- 部分適用を使用しない例
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円)を求める

ずっと気になっていた点だが、お行儀が悪いのか利点があるのか、果たして?

PaalonPaalon

カリー化

関数 f(x, y, z) について、f(x, y, z) = g(x)(y, z) を満たす gf のカリー化という。Haskell の関数はデフォルトでカリー化されているらしい。つまり、f(x, y, z) のように見えるものは f(x)(y)(z) ということだろう。タプルを使うとなんかまずいことがあるんだろうか?

PaalonPaalon

遅延評価

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)

正格評価したいときはどうするんだろう。

PaalonPaalon

とほほの Haskell 入門読了。図書館から借りてきた「Haskell 入門 関数型プログラミング言語の基礎と実践, 本間・類地・逢坂, 2017」を読むことにする。

PaalonPaalon

p. 8

このように、遅延評価では評価の順番が予測困難なので、そのままでは書いた順に評価されてほしいI/Oとの相性がよくありません。これを解決するために、Haskell では IO モナドによって I/O 処理を直列化することで、I/O の発生順を制御します。
...このような巨大の未評価の式が溜まってメモリを消費することを、スペースリークと呼びます。

なるほど。メモリは空間計算量だからスペースリークはメモリリークと実質同じ?

PaalonPaalon

p. 29

リストによる文字列の実装は、リスト用のすべての関数を再利用できるという点で優れています。しかし、Haskell のリストは連結リストであり、速度とメモリ使用量の両面から見て決して効率がいいものではありません。そのため、Haskell でテキストファイル解析や HTML の生成など大きな文字列を扱う場合は、ByteString 型や Text 型を使う必要があります。

なるほど。懸念していた配列はあるのだろうかという問題に対する一つの答えがこれというわけか。Haskell で良い実装をするのは知識とテクニックが必要そうで難しそうだ。

PaalonPaalon

p.30

Unit 型は値を1つだけ持つ型です。型も値も () で表します。

Rust を勉強しているときにも思ったのだが、これはどうなのだろうか。型も値も同一視して同じ記法をするのは記号の濫用で良くないように思ってしまう。

PaalonPaalon

p. 31

*16 Haskell でも base パッケージの Date.Void モジュールに Void 型が定義されています。しかし、これは関数の戻り値の形にできないなど他言語の void や Unit 型とは異なるものです。Void は空集合を表す型であり、pipes パッケージなどで型変数に代入して使われます。また、さらに別のものとして、Control.Monad モジュールには void 関数があり、I/O アクションの戻り値を明示的に捨てるために使われます。

詳しくそれぞれのケースについて学ばないとよく分からないなぁ。

PaalonPaalon

p. 33 ボトムについて。正しく評価できない式をボトムという。評価中にエラーになるものや、無限ループにより計算が終わらないものなどがその例である。文書中では ⊥ で表されることが多い。Haskell では x = xPrelude モジュールの undefinederror がボトムの例である。ボトムは評価されるとエラーになる。逆に言えば、評価されなければ現れてもエラーにはならない。ボトムは任意の型を持てる。ボトムによって発生するエラーはコンパイルの型によるチェックでは見つけられないため安易に使いまくってはいけない。

PaalonPaalon

p.34-38, 変数の宣言方法について。変数が宣言できる場所は

  • トップレベル
  • where
  • let ... in ...

である。やっと let ... in ... の意味が説明された。let の後ろに変数宣言を置き、in の後ろに式を置くとのこと。

p. 36. Haskell ではインデントにはスペースを使う。他の文字でも使えるものがあるらしいが warning 出るとのこと。

PaalonPaalon

p. 40. 演算子の左右結合について。a ∘ b ∘ c について (a ∘ b) ∘ c と解釈されるとき を左結合の演算子といい、a ∘ (b ∘ c) と解釈されるとき を右結合の演算子という。

PaalonPaalon

p. 43.

Haskell では常に返り値は1つなのでタプルで複数個の返り値を表します。

タプルも有効利用するらしい。

PaalonPaalon

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 文字になる。

PaalonPaalon

もっと推し進めると (1₁ + 2) * (1₂ + 3) で 19 文字みたいな文法を考えることができそう。ちなみに Haskell の (. (3 +)) . (*) . (2 +) は 23 文字で十分小さかった。

PaalonPaalon

こっちの定義の方が 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
PaalonPaalon

そろそろ Haskell の勉強に戻ろう。p. 45. 正格評価は先行評価ともいう。遅延評価は非正格評価ともいう。

PaalonPaalon

p.48. 先行評価させる方法について。最初の方法は seq を使う方法。他には $! を使う方法がある。

PaalonPaalon

p. 50. 中置演算子の部分適用を簡潔に記述するためのセクションという記法について。

  • (1 +) で演算子 + の第一引数へ 1 を部分適用した関数を表す。
  • (+ 1) で演算子 + の第二引数へ 1 を部分適用した関数を表す。

前置単項演算子と被るものについては第二引数に関するセクション記法は使えない。
通常の関数の部分適用では第一引数に関するものしか記述できない。
Haskell で単項演算子は - しかないらしい。

PaalonPaalon

p. 55. 純粋な関数内でデバッグのために出力をしたいときには GHC の提供する Debug.Trace モジュールを使う。

PaalonPaalon

p. 58. アズパターン (as pattern)。パターンマッチの際にパターンにマッチした値を変数に束縛したいとき、x'@(_, 0)変数名@パターン と書くと束縛できる。

PaalonPaalon

p. 59. 反駁不可能パターン (irrefutable pattern, lazy pattern)。パターンマッチの際にマッチさせる値がボトムのとき、case 式全体の値もボトムになり、正格評価されてしまうのを防ぎたいとき、パターンの先頭に ~ を付けることで非正格にできる。これを反駁不可能パターンという。

ありがたさは、使ってみないと実感できなさそう。

PaalonPaalon

p. 60. コンストラクターパターン。フィールドへのアクセサーとして使う。

x = (1, "one")
case x of (x1, x2) -> print x2

オブジェクト指向の魂みたいな機能。

PaalonPaalon

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
PaalonPaalon

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"
PaalonPaalon

p. 63. 変数定義におけるパターンマッチ

変数定義でパターンを指定すると反駁不可能パターンとして扱われる。

(x, y) = 22 `divMod` 5
PaalonPaalon

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]
PaalonPaalon

p. 65.

map
map (2 *) [1..10] -- [2,4,6,8,10,12,14,16,18,20]
filter
filter (\n -> n `mod` 3 == 0) [1..10] -- [3,6,9]
foldl 左結合集約
foldl (-) 0 [1..10] -- -55
foldr 右結合集約
foldr (-) 0 [1..10] -- -5
PaalonPaalon

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

p.68 Either

Either 型はエラーが発生するかもしれない処理の返り値として使われます。Either 型の値を作るコンストラクタは2つあり、正常時の値は Right、異常時の値は Left で包んで表現します。

なるほど。RightLeft の謎が解けた。ということは Julia で言うところの単なる Union{A, B} とは意味が違う。Union{SomeError, A} というところか。

Data.EitherEither 型を操作する関数が定義されている。

PaalonPaalon

p.68-71. ループ

  • 再帰
  • map
  • filter
  • foldl
  • foldr
  • Control.MonadforM_
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 BA が真のとき B をする ⇔ Julia の A && B
  • unless A BA が偽のとき B をする ⇔ Julia の A || B

ということのようだ。入門書なら習っていない制御文が出てくるのはやめて欲しい。

PaalonPaalon

p. 71-80. モジュールとパッケージ

Haskell にはプログラムを複数のファイルに分割して書くための単位として、モジュールとパッケージがあります。GHC においては、モジュールはファイル単位でのプログラム分割、パッケージは複数のモジュールをまとめたライブラリの単位でのプログラム分割を担います。

出た!Python とか Java と同じでこれは悪いモジュール機能だ。ファイル単位とモジュール単位が結びついていて分離できないインターフェイスだ。ファイル単位でモジュール分割するのはやめて欲しい。プログラムとしての姿をディレクトリー・ファイルの構造と極力結び付けないで欲しい。

GHC においては、モジュール名とファイル名は一対一で対応します。モジュール名は大文字から始まり、2文字目以降は変数名と同様 Unicode まで含めた英数字と ' が続きます。そして . で区切ることで階層化できます。MyApp.MyModel.MyUtil のようなものをモジュール名として使えます。

んーと、日本語とかも使えるのかな?でも変数名のところには使えないって書いてあったし多分使えないのだろう。ここの説明は微妙だな。Go もそうだが、変数名にきつい制約があるのはやめて欲しい。自由な言語としてどうなんだ。

パッケージは Cabal 形式で作成する。レポジトリには Hackage と Stackage がある。この本は Stackage の使用を推奨している。パッケージのバージョニングはドット区切りの4桁の数字?なのかな。セマンティックバージョニングとは異なるようだ。

Haddock というソースコードにコメントでドキュメントを書く形式があるようだ。
第1章のはじめての Haskell と第2章の基本の文法までを読了した。

PaalonPaalon

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) 的な?

IOMaybe はカインドが * でないので正確には型ではありません

次のページにカインドの説明が書いてあるので読む。

PaalonPaalon

p. 88-89. カインド。

型を組み合わせる際のルールを規定するために使うものをカインド (kind) と呼びます。
カインドは基本的には * (star) と -> だけで表され、型コンストラクタと型引数の組み合わせを規定します。

IntString のカインドは * である。IO のカインドは * -> * である。

* のカインドを持つ型コンストラクタや、型コンストラクタの組み合わせは一般にと呼ばれます。

正確には型ではありませんの話は分かった。

PaalonPaalon

p. 90. 3.3.2 型変数。

型の変数を型変数 (type variable) という。慣習的に小文字一文字を使うことが多い。型変数を特定の型クラスに所属するものに制約できる。これを型制約 (type constraint) という。これは 式 :: 型クラス 型変数 => 型 と書く。

show :: Show a => a -> String
PaalonPaalon

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 ...
PaalonPaalon

p. 94. 正格性フラグ。

デフォルトだとコンストラクタはすべての引数について非正格だが、コンストラクタの定義において型の前に正格性フラグ ! を付けると、その引数について正格になる。

PaalonPaalon

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

フィールド名は名前空間を汚すらしい。

PaalonPaalon

p. 103. 型の別名

型名が長過ぎて可読性を損ねていたり、同じ形式であってもデータの保つ意味が違ったりする場合、型に別の名前を付けられると便利です。
前者の目的では type キーワードを、後者の目的では newtype キーワードを使います。

なるほど。

p. 107.

newtype は型が区別される以外は type と同等であるため、値の変換コストがまったくないことが保証されています。

typenewtype をどういうときに使うのかは分かった気がする。

  • type 新しい型名 = 元にする型名
  • newtype 型コンストラクタ名 = データコンストラクタ名 型
PaalonPaalon

p.107. 型クラス。

型クラスに所属する型を、その型クラスのインスタンス (instance) という。ひとまず、型クラスは型制約に使える、複数の型をまとめるためのカテゴリーだと思えばいいらしい。そうだとやはり C++ や Nim のコンセプトや、Julia の抽象型みたいなもんか?Rust のトレイト的な?

PaalonPaalon

p. 108. 型クラスで定義され、任意の型によって具体的に実装される、型クラスに紐付いた関数をメソッドという。
Java と Haskell の用語の対応は以下の感じらしい。

Java Haskell
インタフェース 型クラス
具象クラス インスタンス
インスタンス

まあ私の認識はそんなに間違っていなさそうだ。

PaalonPaalon

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

まあこれはいいだろう。不思議なところはない。

PaalonPaalon

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. に関してモノイド。

代数学の初歩が分からない人には分からなさそう。

PaalonPaalon

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 宣言における型制約の例はなかなか見慣れなくて私にはまだ難しい。これが関数型プログラミングかという感じ。しばらくやったら慣れるかな?やっぱ自然と部分適用が駆使されていることの認知負荷が高い。

PaalonPaalon

p. 117-119. 型変数の曖昧性。

hellobye の例は、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 と他に何と曖昧になるのかが分からない。

PaalonPaalon

一般過ぎる型の例。

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

asTypeOfconst と似ているらしいが、const なんて今まで出てこなかったのだから、何のことか分かるわけがない。分からないので飛ばす。

PaalonPaalon

Haskell 2010 に従うと、String 型を型クラスのインスタンスにはできないらしい。インスタンス宣言に型シノニムを使えないらしい。しかし、[Char] を使ってもインスタンスの定義はできないらしい。解決策は newtype によって別の型を定義することらしい。定義できないようにしている理由は曖昧性が生じるのを防ぐためらしい。上記の曖昧性を理解できてから考えればいいや。

PaalonPaalon

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

なんか数値型はごにょごにょ書いてあるが、よく分からないので必要になったら覚えよう。

PaalonPaalon

p. 130. deriving によるインスタンス定義。

deriving はコンパイラがサポートしている特定の型クラスに対して、自動的にインスタンス定義を作ってくれる機能です。

deriving で指定できる型クラスは Prelude モジュールの EqOrdEnumBoundedShowRead、そして Data.Ix モジュールの Ix です。計7つしかありません。

えー?ユーザーが自由に derive できないらしい。

第3章読了。型変数の曖昧性のところだけしっくりこなかった。

PaalonPaalon

疲れてきたのでHaskellでの型レベルプログラミングをぱらぱらと見る。

古典的なHaskell(Haskell 2010)ではカインドは * と -> からなるものだけですが、現代のGHCではそれ以外のカインドも使用できます(実際のところ、DataKinds拡張により任意のデータ型をカインドとして用いることができます)。詳しくは後述します。

Haskell 2010 は古典的な Haskell らしい。GHC 拡張に踏み込みすぎないほうが良いのかと思ったがそんなことはないようだ。

PaalonPaalon

GHCi を使ってなかったが便利そうなので使おうか。

第4章 I/O 処理も大事な章だと思うがぱらぱらと読んだ。後で詳しくやろう。第5章のモナドに進む。

p. 163-164. モナド。

Haskell では Monad 型クラスのインスタンスでモナド則という一定の規則を満たしている型をモナドと呼びます。

モナドはその「計算」に対して手続き的にプログラムを組み上げる方法を提供します。

私は圏論は理解できていない。

PaalonPaalon

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 におけるモナドが提供する機能はそれだけです。それ以上でもそれ以下でもありません。

それなら話はそんなに難しくなさそうだなぁ。

PaalonPaalon

やっぱり第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

>>>>= の値を捨てる版。

PaalonPaalon

p.138 コマンドライン引数。

import System.Environment
main = do
  args <- getArgs
  print args
PaalonPaalon

p. 138-139. 環境変数。

System.Environment

  • getEnv :: String -> IO String
  • lookupEnv :: String -> IO (Maybe String)
  • setEnv
  • unsetEnv

を使う。

PaalonPaalon

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 モジュールで提供されている。

PaalonPaalon

p. 154-162. 例外処理。

基本は MaybeEither を使うのがお上品。
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 を使う。