まだ型のない言語で消耗してるの?
静的型付け言語を使おう!
Haskellを触ったことがある人にありがちなこと
- リスト とか
map
とかfold
とかzip
だけやたら詳しい - REPLしか触ったことがない
- ファンクタ、モナドとはなにかググり始めてしまった
Haskellはプログラミング言語です!
実用言語としてのHaskell
- 高度に抽象な概念はデザパタやビルトイン機能のようなもの
- プログラムを書けるようになってから振り返ったほうが学習効率が良い
開発環境
-
Stack
- いい感じに色々裏でやってくれるビルドツール
- コンパイラGHCのインストールも任せたほうがいい
-
Intero
- emacs をいい感じに使えるようにするモード
- stackによる自動セットアップ
- 補完とかジャンプとかflymakeとか
- vim使ってる人はいませんよね?
ライブラリ
-
stackage LTS
- stackで依存でハマらずにビルドできるライブラリ群
-
hackage
- stackageにないものもある
- 依存の解決は自己責任
ドキュメント
- GHC のマニュアル
- haskell-lang.org
- 保守されてないかも?
-
lotz84さんとこ
- 雑多なブックマーク的なの。メンテされてる。
カジュアルな実行
stack runghc hello.hs
main = putStrLn "Hello!"
または
$ echo 'main = putStrLn "Hello!"' | stack runghc
Hello!
Perlの関数の型
sub div_mod {
my ($x, $y) = @_;
my $div;
{
use integer;
$div = $x / $y;
}
($div, $x - $div*$y);
}
Perlの関数の型
Perl の全ての関数は副作用を書けるので、戻り値を IO
で包む。
perlDivMod :: Int -> Int -> IO (Int, Int)
perlDivMod x y = do
let d = x `div` y
return (d, x - d * y)
IO
を恐れない
IO
を使わない純粋な関数は奨励されるが、以下を覚悟する。覚悟できないなら IO
を恐れず使う。
-
print
デバグができない - 設定ファイルを読めない
- 環境変数を読めない
- ログを吐けない
- 現在時刻を取得できない
-
IO
を使うよう定義してしまった自前の関数は一切呼べない -
IO
を使わないと決めた関数から呼び出す予定のすべての関数が、これらの制約をすべて受ける
do
記法
Haskellを手続き型言語に変える糖衣構文。
-
IO
型を並べると、順に実行してくれる -
IO
型に他のモナドを被せた、「IO
」が可能なモナドも同じように使え、多用される- 例:
ReaderT Context IO
、StateT State (ReaderT Context IO)
など -
liftIO
でIO
の呼び出しが可能
- 例:
-
IO
型以外を記述した場合の振る舞いには要注意
IO
関数の戻り値を使う場合の注意
sub fizz { "Fizz" }
sub buzz { "Buzz" }
sub fizz_buzz { fizz . buzz }
print fizz_buzz;
IO
関数の戻り値を使う場合の注意
IO
型では、関数の戻りとを直接 <>
の引数として使う fizz <> buzz
のような記述はできない。面倒でも <-
で IO
を剥がす。
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid ((<>))
import qualified Data.Text.IO as Text
fizz = return "Fizz"
buzz = return "Buzz"
fizzBuzz = do
fizzStr <- fizz
buzzStr <- buzz
return $ fizzStr <> buzzStr
main = do
fizzBuzzStr <- fizzBuzz
Text.putStrLn fizzBuzzStr
IO
関数の戻り値を使う場合の注意
<$>
<*>
=<<
関数を使いこなせる人ならこう。
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid ((<>))
import qualified Data.Text.IO as Text
fizz = return "Fizz"
buzz = return "Buzz"
fizzBuzz = (<>) <$> fizz <*> buzz
main = Text.putStrLn =<< fizzBuzz
が、格好つけて書こうとするとハマりやすい。
ミュータブル変数
sub is_divisible_by {
my ($x, $y) = @_;
my $divisible = 0;
$divisible = 1 if $x % $y == 0;
return $divisible;
}
print is_divisible_by(8, 7) ? "Divisible by 7\n" : "Not divisible by 7\n";
ミュータブル変数
イミュータブル変数で定義し直す。
done'
という別の変数に変更後の値を入れている。
isDivisibleBy x y = do
let divisible = False
divisible' = if x `mod` y == 0 then True
else divisible
return divisible'
main = do
divisible <- isDivisibleBy 8 7
putStrLn $ if divisible then "Divisible by 7"
else "Not divisible by 7"
ループ
- 再帰で書き直す
-
last
は再帰をやめる -
next
は後続の処理をせずに再帰する - ループ中に変化する状態は、引数と戻り値で表す
ループ
my $n = 0;
while (<>) {
chomp;
last if $_ eq 'last';
next if $_ eq 'next';
print ++$n, ": $_\n";
}
print "Got ", $n, " lines.\n"
ループ
import Data.Monoid ((<>))
import System.IO (isEOF)
main = do
n <- loop 0
putStrLn $ "Got " <> show n <> " lines."
where
loop n = do
l <- getLine'
case l of
Nothing -> return n
Just "last" -> return n
Just "next" -> loop n
Just l' -> do let n' = n + 1
putStrLn $ show n' <> ": " <> l'
loop n'
-- helper
getLine' = do
eof <- isEOF
if eof then return Nothing
else Just <$> getLine
遅延リストによるI/Oという幻想
- 遅延リストに
map
filter
fold
でI/Oを美しく扱えるというのは幻想 -
IO [String]
では入力ストリームは表せない- 一回の
IO
で遅延リスト1個を作れるという型 - ファイルを読む(I/O)ことを何度も遅延して行うのは無理
-
unsafeInterleaveIO
という邪悪な何かが必要
- 一回の
- ストリームライブラリが必要
遅延リストによるI/Oという幻想
import Data.Monoid ((<>))
import Pipes ((>->))
import qualified Pipes.Prelude as Pipes
main = do
n <- foldM' step 0
. filter' (/= "next")
. takeWhile' (/= "last")
$ Pipes.stdinLn
putStrLn $ "Got " <> show n <> " lines."
where
step n s = do
let n' = n + 1
putStrLn $ show n' <> ": " <> s
return n'
-- helper functions
foldM' f a = Pipes.foldM f (return a) return
takeWhile' p = (>-> Pipes.takeWhile p)
filter' p = (>-> Pipes.filter p)
グローバル変数
- アプリを作ろうとすると、自然とグローバル変数が出てくる
- アプリの設定とか
- ロガーとか
- そういう諸々のシングルトンインスタンスとか
-
ReaderT Ctx IO
というモナドの元でアプリを作るのが定石
グローバル変数
my $counter = 0;
sub count {
print "count ", ++$counter, "\n";
}
count;
count;
グローバル変数
- アプリ内で共有したい状態(
$count
など)はまとめてdata
定義 - ミュータブルな値は
IORef
で定義するが、main
内で初期化が必要 -
ReaderT Ctx IO
モナド下では、asks
関数でいつでも自由にCtx
内の変数に触れる -
IO
もliftIO
で任意の箇所で可能
グローバル変数
import Data.IORef (IORef, newIORef, modifyIORef, readIORef)
import Control.Monad.Trans.Reader (runReaderT, asks)
import Control.Monad.IO.Class (liftIO)
data Ctx = Ctx { counter :: IORef Int }
main = do
ref <- newIORef 0
runReaderT main' $ Ctx { counter = ref }
count = do
ref <- asks counter
liftIO $ do
modifyIORef ref (+ 1)
n <- readIORef ref
putStrLn $ "count " ++ show n
main' = do
count
count
まとめ
- Haskellで普通のアプリを書くためのTipsを紹介した
-
IO
を恐れず、格好つけずに<-
を使う - ミュータブル変数は代入箇所をイミュータブルで定義し直す
- ループは再帰で
- 状態を持つアプリを作るときは
ReaderT Ctx IO
が定石
-
- CONS:
-
<-
による関数呼出しが必須なので冗長になりがち -
next
やlast
を愚直に再帰で表すと冗長になりがち -
IORef
によるミュータブル変数は冗長になりがち
-
- PROS:
- まったく型を書いてないが、型安全が保証されている
- 最初は冗長でも、抽象概念を学べば学ぶほどコードは短くなっていく