Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

今日は「Haskell入門ハンズオン!」の日です

今日は「Haskell入門ハンズオン!」の日です

https://shinjukuhs.connpass.com/event/58224/へメンター枠で来ていますので、自分用のメモを残しておく。

はじめに / 木下さん

  • #Haskel
  • 講師重城さん+メンター5人居ます
  • haskellでわからないことはteratailで質問しましょう
  • 8/28 に Haskell入門者LT大会やるよ

Haskellの概要 / 重城さん

  • 自己紹介 : Gentoo on KVM on Gentoo, TUT-code, Xmonad, rxvt-unicode, Tmux, Mutt, HHKB
  • ハンズオンの中身 : 完全入門者向けだけど、Haskellの面白さは伝えたい
  • stack --version stack ghci :quit
  • 4492 'c' pi 電卓的な使い方、 it
  • カーソルキーで履歴を辿れる
  • repeat "Haskell" からの Ctrl+C
  • サンプルコード
  • pwdcdディレクトリ移動して、 stack ghci
  • 行儀が悪いのでTABを8文字幅で使うよ!(本当は4スペースが行儀がいい)
  • myFavoriteFruit = "apple":load で読み込み
  • "banana" に変更してmyFavoriteFruit` をしても変わらない
  • :reload すると切り替わる
  • double x = x * 2 関数
  • (\x -> x * 2) 8 関数リテラル
  • 演算子と関数は同じ
  • 演算子は中置で記号列。 () で関数になる
  • 関数は `` でe演算子になる
  • lucky :: Integer 型宣言
  • Maybe Just Nothing
  • タプル taro = ("Taro Yamada", 35)
  • パターンマッチ
    • helloTo (Just n) = "Hello, " ++ n ++ "!"
    • helloTo Nothing = "Hello, customer!"
    • human (n, a) = n ++ "(" ++ show a ++ ")"
    • リテラル safeRecip 0 = Nothing
    • ワイルドカード isNothing (Just _) = False
    • アズパターン atPattern jx@(Just x) = show jx ++ ": " ++ show x
  • "" は文字列、 '' は文字
  • パターンが抜けているときは部分関数になる。マッチしないと例外
  • 例外はI/Oモナドで拾える
  • 「代入じゃなくて束縛と言わないと怒られる」
  • show は多相関数なので Maybe Integer にも Integer にも使える
  • ガード safeSqrt x | x < 0 = Nothing
  • case式 yesNo c = case c of 'n' -> Just False
  • 多相: ignoreSecond :: a -> b -> a ignoreSecond x y = x
  • 型シノニム: type Human = (String, Integer)
  • モジュールの導入 import Data.Maybe (fromMaybe)

ここで休憩。

  • 多相関数を使うと、ソースコードがきれいになる
  • const の2つの側面
    • 第二引数を無視する関数
    • 常に定数を返す関数 (const 関数への部分適用)
    • ignoreSecond と同じ定義
  • 関数適用演算子 $
    • f x なので必要ない
    • ($) f x
    • recipe $ 3 + 5 カッコが取れる
  • 部分適用
    • bmi h w = w / (h / 100) ^ 2
    • bmiTaro' = bmi 170 2つある引数のうちひとつだけを与えた
    • (125 /) 第一引数に対する部分適用
    • (/ 5) 第二引数に対する部分適用
  • 関数合成 : (* 2) . (+ 3)
    • 関数は後ろから適用される
  • 繰り返し
    • 再帰よりリストは簡単
    • sumN n = sum [1 .. n]
    • 3の倍数以外の総和 filter ((/= 0) . (`mod` 3))
    • 5で割ったあまりの総和 map (`mod` 5)
  • モンテカルロ法の例
    • 正方形内の点を列挙、円に入るもののみをろ過、残った点を数える
  • 無限リスト take 10 [1 ..]
    • 必要になるまで評価しないので、無限時間はかからない
    • 処理と終了条件を分離できる
  • リストへの追加 5 : [3, 2, 9]
  • 空リスト []
  • パターンマッチ headTail (x : xs)
  • 2段のリストを1段に concat [[1,2,3], [4,5], [6,7]]
  • すべての組み合わせで concatMap
    • (`concatMap` [1, 2, 3]) $ \x -> (`concatMap` [4, 5, 6]) $ \y -> (`concatMap` [7, 8]) $ \z -> [x * y * z]
  • 糖衣構文 リスト内包表記 [x * y * z | x <- [1,2,3], y <- [4, 5, 6], z <- [7, 8]]

時間がないので再帰は飛ばすことに。「近現代史は教科書読んでおいてね、と同じ」。

  • Haskell以外の言語では、式の評価と入出力が分けられている
    • 3 + 5 を評価しても、誰かの銀行口座が0円になることはない
  • 評価と実行は分かれている (が、対話環境が隠している)
    • 対話環境は、評価結果が入出力をする「機械」だったときに実行する
  • putStrLn は文字列を引数として取り、それを表示する機械を返す
  • getLine IO String 機械が出した結果を対話環境が表示
  • 値を機械から機械へ渡す >>=
  • getLine >>= \x -> で改行をして書くとわかりやすい
  • 糖衣構文 do 記法
    • >>=>> を隠している
  • 実行可能ファイルを作る stack ghc -- *.hs
    • TAB使いなので -fno-warn-tabs
  • 標準入力からの入力を変換するプログラムの書き方
    • interact
    • 3回 love というと I love you しか返ってこなくなる例

ここからは各自でハンズオン。

去年の9月にconduitの演算子が変わっていた

conduitといえば $$$= =$= のような演算子を使い分けなければいけなくて 面倒だなものだと思っていた のだが、去年の9月から .| だけを使えばいいように変わっていた。これは直感的で使いやすい。

module Main (main) where
import Data.Char (ord, chr, toUpper)
import Data.Conduit (Conduit, runConduit, (.|))
import qualified Data.Conduit.Binary as B
import qualified Data.Conduit.List as L
import qualified Data.ByteString as BS
import Data.Word (Word8)
import System.IO (stdin, stdout)

main :: IO ()
main = runConduit $ do
  B.sourceHandle stdin
    .| B.takeWhile (\c -> c /= word8 '.')
    .| filterUc
    .| B.sinkHandle stdout
  return ()
  where
    word8 = fromIntegral . ord

filterUc :: Monad m => Conduit BS.ByteString m BS.ByteString
filterUc = L.map ucString
  where
    uc :: Word8 -> Word8
    uc = fromIntegral . ord . toUpper . chr . fromIntegral
    ucString :: BS.ByteString -> BS.ByteString
    ucString = BS.map uc

演算子との対応関係は README にきちんと書いてある。

自由モナドの定義であるところの Control.Monad.Free.Church.foldF

圏論勉強会の資料 によれば、 X と自由な構成 FXについて、 f :: X \to |Y| を与えると \overline{f} :: FX \to Y が得られるとある。

自由モナドの文脈でこれを考えると、関手 X からモナド Y (の構造を忘れて関手と思ったもの)への自然変換を定義すれば、自由モナド FX からモナド Y への自然変換(正確にはモナドモーフィズム)が得られるという意味となる。

free パッケージにこの対応関係に相当するものは入ってないのかなと探してみたら、 Control.Monad.Free.Church というモジュールで定義されていた。

foldF :: Monad m => (forall x. f x -> m x) -> F f a -> m a

The very definition of a free monad is that given a natural transformation you get a monad homomorphism.

https://www.stackage.org/haddock/lts-8.19/free-4.12.4/Control-Monad-Free-Church.html

あまり深く追ってないけど、このモジュールで定義されているのは内部表現が違う(チャーチエンコーディングされた)自由モナドらしい。後、モナド変換子版 FreeT ではだめでモナド Free じゃないと定義できないということもありそう。

これを使うと、 FunctorMonad としてどう解釈するかを定義するだけで、自由モナドから任意のモナドへの変換が得られる。

{-# LANGUAGE TypeApplications #-}
module Main (main) where
import qualified Control.Monad.Free.Church as F

data HelloProgram a = HPGetLine (String -> a) | HPPrint !String a

instance Functor HelloProgram where
  fmap f (HPGetLine g) = HPGetLine (f . g)
  fmap f (HPPrint s x) = HPPrint s (f x)

helloApp :: F.F HelloProgram ()
helloApp = do
  line <- hellGetLine
  hellPrint line
  where
    liftF' = F.liftF @HelloProgram @(F.F HelloProgram)
    hellGetLine = liftF' $ HPGetLine id
    hellPrint s = liftF' $ HPPrint s ()

toIO :: HelloProgram a -> IO a
toIO (HPGetLine f) = f <$> getLine
toIO (HPPrint ss x) = putStrLn ss >> return x

main :: IO ()
main = F.foldF toIO helloApp

よくある自由モナドの使い方では、変換規則は自由モナド F.F HelloProgram に対して直接用意するが、このコード例では関手 HelloProgram の自然変換のみを変換規則として定義している。この方法では自由モナドの構造が使えないので、関手側に Done のようなデータを用意してそこで処理を打ち切る、といったような解釈の仕方を定義することはできない。そのようなことが必要であれば、 MaybeT IO モナドなど、それ相応の機能を持つモナドへ変換する必要がある。

IOモナドで使うときだけログを吐く関数を定義する

純粋な関数として定義できるんだけど内部でやってることが複雑な場合、何が起きてるかわからないと心配だからとログを吐く機能をつけると、その時点でそいつは IO アクションになってしまう。ログを吐くという副作用を持つのだから IO になるのは当たり前でそれを避けるべきではないのだけど、ログを吐かなくていいいシチュエーションでは、その計算を純粋な関数として使えたほうが理想的ではある。

そんなことを Identity型クラス 使えば簡単にできるんじゃねと思いついたんだけど、 monad-loggerそもそも機能が提供されてた。

runLoggingTrunNoLoggingTモナドclass MonadLogger が持つロギング用のアクションを追加できるのだけど、前者はモナドclass MonadIO のとき、後者は任意の class Monad について使えるようインスタンスが定義されている。

以下の例で addM は足し算するだけのアクションだが、引数をロギングするようになっている。 addaddMIdentity モナドを代入してそれを純粋な関数にしたもの1

{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad.Logger
import Criterion
import Criterion.Main
import Data.Monoid ((<>))
import qualified Data.Text as TX
import Data.Functor.Identity

showT :: Show a => a -> TX.Text
showT = TX.pack . show

addM :: (MonadLogger m, Num a, Show a) => a -> a -> m a
addM x y = do
  logDebugN $ "x=" <> showT x <> ", y=" <> showT y
  return $ x + y

add :: (Num a, Show a) => a -> a -> a
add x y = runIdentity . runNoLoggingT $ addM x y

main :: IO ()
main = do
  runStdoutLoggingT $ do
    n <- addM 1 2
    logDebugN $ showT (n :: Int)
  
  defaultMain
    [ bgroup "add" 
      [ bench "add" $ whnf (add 1) (2 :: Int)
      , bench "(+)" $ whnf (1 +) (2 :: Int)
      ]
    ]

素の足し算と比べるとオーバヘッドはある。

$ stack ghc -- -o pure-logger pure-logger.hs
[1 of 1] Compiling Main             ( pure-logger.hs, pure-logger.o )
Linking pure-logger ...
$ ./pure-logger
[Debug] x=1, y=2
[Debug] 3
benchmarking add/add
time                 87.08 ns   (83.77 ns .. 91.27 ns)
                     0.988 R²   (0.975 R² .. 0.999 R²)
mean                 84.59 ns   (82.95 ns .. 87.74 ns)
std dev              7.355 ns   (4.199 ns .. 12.95 ns)
variance introduced by outliers: 88% (severely inflated)

benchmarking add/(+)
time                 22.31 ns   (20.54 ns .. 23.96 ns)
                     0.970 R²   (0.964 R² .. 0.980 R²)
mean                 21.62 ns   (20.51 ns .. 22.97 ns)
std dev              3.736 ns   (3.185 ns .. 4.343 ns)
variance introduced by outliers: 97% (severely inflated)

が、 -Oコンパイルしたところ、オーバヘッドはきれいに消え去った。やはり最適化がきちんと効くとGHCは強い。

$ stack ghc -- -O -o pure-logger pure-logger.hs
[1 of 1] Compiling Main             ( pure-logger.hs, pure-logger.o )
Linking pure-logger ...
$ ./pure-logger
[Debug] x=1, y=2
[Debug] 3
benchmarking add/add
time                 5.809 ns   (5.663 ns .. 5.981 ns)
                     0.997 R²   (0.995 R² .. 0.999 R²)
mean                 5.728 ns   (5.662 ns .. 5.835 ns)
std dev              277.3 ps   (193.9 ps .. 404.2 ps)
variance introduced by outliers: 74% (severely inflated)

benchmarking add/(+)
time                 6.036 ns   (5.986 ns .. 6.096 ns)
                     0.999 R²   (0.998 R² .. 0.999 R²)
mean                 6.163 ns   (6.090 ns .. 6.334 ns)
std dev              367.0 ps   (221.5 ps .. 643.8 ps)
variance introduced by outliers: 81% (severely inflated)

これでログを吐きつつ計算するI/Oアクションと純粋な関数が、一切のオーバヘッドなしに同時に手に入った。


  1. Show 制約がはずれないのは悲しいが、ロギングするための項が入ってしまっているので仕方ないだろう。あるいは、この項が型ごと差し替えられるようになれば可能なのかな?

C言語の多次元配列の型はどう読むのか

int a[2][3] って、「整数2個の配列( int a[2] )を 3個の配列( [3] )にした、と読めるけどどうなのか1http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf と比べて確認。

116ページのArray declaratorsの節によると、 T D[n] という形式のときは「… array of T」となると書かれている。ここで T は int で D は a[2]n3 なので、「整数3個の配列… 」となる。 … の部分は仕様上で “derived-declarator-type-list” と呼ばれている部分で、 T D がどういう型かによって定められる。 int a[2] は「2-array of int」だけど、 … は最後の int は除くことになっているので、まとめると「2-array of 3-array of int」であって、「整数3個の配列の2個の配列」という意味になる。

なんとも歯切れの悪い定義だけど、 int の何かを定義しているってことでこうなっているのだろうか。考え方としては、 「int D[3]」 みたいな定義があるともうこれは整数3個からなる配列であり、Dをどう書くかでそいつをどう調理するかが決まるってだけ。 a[2] と書けばそれを 2 個の配列にするし、 *a って書けばそいつへのポインタになる。


  1. 添字でのアクセスを考えると違うだろってのはすぐ想像つくけど。

man introとcalコマンド

月末って何曜日だっけとかってときいつもgoogleカレンダー開いてたんだけど、これでいいじゃん。知らなかった・・・。

ubuntu:~$ cal
      May 2017
Su Mo Tu We Th Fr Sa
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31

man 1 intro したら書いてあった。 man intro なんて初めて使ったし、そもそも man の基本的な使い方もわかってないよね。適切な情報を得る術を知らずに何年も費やしても、まともな知識は得られないってことだね。