Haskell
YAPC

【YAPC::Hokkaido 2016 前夜祭】 From Perl to Haskell

More than 1 year has passed since last update.


まだ型のない言語で消耗してるの?



静的型付け言語を使おう!



Haskellを触ったことがある人にありがちなこと


  • リスト とか map とか fold とか zip だけやたら詳しい

  • REPLしか触ったことがない

  • ファンクタ、モナドとはなにかググり始めてしまった



Haskellはプログラミング言語です!



実用言語としてのHaskell


  • 高度に抽象な概念はデザパタやビルトイン機能のようなもの

  • プログラムを書けるようになってから振り返ったほうが学習効率が良い



開発環境



  • Stack


    • いい感じに色々裏でやってくれるビルドツール

    • コンパイラGHCのインストールも任せたほうがいい




  • Intero


    • emacs をいい感じに使えるようにするモード

    • stackによる自動セットアップ

    • 補完とかジャンプとかflymakeとか

    • vim使ってる人はいませんよね?





ライブラリ



  • stackage LTS


    • stackで依存でハマらずにビルドできるライブラリ群




  • hackage


    • stackageにないものもある

    • 依存の解決は自己責任





ドキュメント



カジュアルな実行


  • 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 IOStateT State (ReaderT Context IO) など


    • liftIOIO の呼び出しが可能




  • 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 内の変数に触れる


  • IOliftIO で任意の箇所で可能



グローバル変数

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:



    • <- による関数呼出しが必須なので冗長になりがち


    • nextlast を愚直に再帰で表すと冗長になりがち


    • IORef によるミュータブル変数は冗長になりがち



  • PROS:


    • まったく型を書いてないが、型安全が保証されている

    • 最初は冗長でも、抽象概念を学べば学ぶほどコードは短くなっていく