おとなのビルコン へ来ていますので、自分用のメモを残しておきます。
- めっちゃ混んでる。大規模カンファレンス感
- 「すげえ!」
- 「来てよかった!」
おとなのビルコン へ来ていますので、自分用のメモを残しておきます。
今日は「Haskell入門ハンズオン!」の日です
https://shinjukuhs.connpass.com/event/58224/へメンター枠で来ていますので、自分用のメモを残しておく。
stack --version
stack ghci
:quit
4492
'c'
pi
電卓的な使い方、 it
repeat "Haskell"
からの Ctrl+Cpwd
や cd
でディレクトリ移動して、 stack ghci
myFavoriteFruit = "apple"
を :load
で読み込み"banana" に変更して
myFavoriteFruit` をしても変わらない:reload
すると切り替わるdouble x = x * 2
関数(\x -> x * 2) 8
関数リテラル()
で関数になる``
でe演算子になるlucky :: Integer
型宣言Maybe
Just
Nothing
taro = ("Taro Yamada", 35)
""
は文字列、 ''
は文字show
は多相関数なので Maybe Integer
にも Integer
にも使えるsafeSqrt x | x < 0 = Nothing
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]
filter ((/= 0) . (`mod` 3))
map (`mod` 5)
take 10
[1 ..]
5 : [3, 2, 9]
[]
headTail (x : xs)
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]]
時間がないので再帰は飛ばすことに。「近現代史は教科書読んでおいてね、と同じ」。
3 + 5
を評価しても、誰かの銀行口座が0円になることはないputStrLn
は文字列を引数として取り、それを表示する機械を返すgetLine
IO String
機械が出した結果を対話環境が表示>>=
getLine >>= \x ->
で改行をして書くとわかりやすいdo
記法
>>=
や >>
を隠しているstack ghc -- *.hs
-fno-warn-tabs
interact
love
というと I love you
しか返ってこなくなる例ここからは各自でハンズオン。
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
圏論勉強会の資料 によれば、 と自由な構成 について、 を与えると が得られるとある。
自由モナドの文脈でこれを考えると、関手 からモナド (の構造を忘れて関手と思ったもの)への自然変換を定義すれば、自由モナド からモナド への自然変換(正確にはモナドモーフィズム)が得られるという意味となる。
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
じゃないと定義できないということもありそう。
これを使うと、 Functor
を Monad
としてどう解釈するかを定義するだけで、自由モナドから任意のモナドへの変換が得られる。
{-# 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
になるのは当たり前でそれを避けるべきではないのだけど、ログを吐かなくていいいシチュエーションでは、その計算を純粋な関数として使えたほうが理想的ではある。
そんなことを Identity
と 型クラス
使えば簡単にできるんじゃねと思いついたんだけど、 monad-logger でそもそも機能が提供されてた。
runLoggingT
や runNoLoggingT
でモナドに class MonadLogger
が持つロギング用のアクションを追加できるのだけど、前者はモナドが class MonadIO
のとき、後者は任意の class Monad
について使えるようインスタンスが定義されている。
以下の例で addM
は足し算するだけのアクションだが、引数をロギングするようになっている。 add
は addM
へ Identity
モナドを代入してそれを純粋な関数にしたもの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アクションと純粋な関数が、一切のオーバヘッドなしに同時に手に入った。
Show
制約がはずれないのは悲しいが、ロギングするための項が入ってしまっているので仕方ないだろう。あるいは、この項が型ごと差し替えられるようになれば可能なのかな?↩
int a[2][3]
って、「整数2個の配列( int a[2]
)を 3個の配列( [3]
)にした、と読めるけどどうなのか1。 http://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]
、 n
は 3
なので、「整数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
って書けばそいつへのポインタになる。
添字でのアクセスを考えると違うだろってのはすぐ想像つくけど。↩
月末って何曜日だっけとかってときいつも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
の基本的な使い方もわかってないよね。適切な情報を得る術を知らずに何年も費やしても、まともな知識は得られないってことだね。