kazzix.com

Haskellに入門してみる

Haskell に入門してみる。 Haskell に入門するのはこれが初めてではなく、高専の 2 年次のあたりで一度書いてみたものの、諦めてしまった。理由は Monad の理解ができなかったことだったと思う。と言ってもほとんど何も覚えていないのだけど。

新しい言語やフレームワークを使い始めるときは毎回題材に迷ってしまう。ほぼ扱えない(すでに扱えたら入門していない)道具で、自分が作りたいものを作る気にもならない。 そこで、近頃知った Advent of Code 2022 というパズルを Haskell で解いてみようと思う。

開発環境の構築

Haskell では Cabal という package manager が普及しているらしく、Rust の Cargo や Ruby の RubyGems と同じような立ち位置らしい。 しかし Cabal では, Cabal Hell と呼ばれる依存関係の問題が起きるらしく、よくわからないけどなんとなく嫌である。 Stack と呼ばれる別のシステムを使うと、Cabal Hell からは逃げられそうであることはわかったので、今回は Stack を使ってみようと思う。 Stack のうれしさはこれを読むとすこしわかりそう。

Advent of Code 2022 Day 1 を解いてみる

これ。 サンタさんのトナカイは、クリスマスにstar fruitをたべてmagical energyを補給するらしい。 エルフがstar fruitを採れるジャングルに僕を連れてきてくれたので、2022 年 12 月 25 日 までにできるだけ多くのstar fruitを採取しなければいけないが、今は 2023 年 1 月 4 日。 トナカイさんごめんなさい。

ジャングルは生い茂っていて乗り物では移動できず、空からも見えないので、——問題文の内容を書いていたけれど、清書するのがめんどくさいので吹き飛ばした。

Stack の Project を作成する

Stack のガイドを読みながらすすめた。 まず ↓ を実行し、Day 1 の project を作成した。

1stack new day1 new-template

次に ↓ を実行し、build してみた。GHC のダウンロードから始まったので意外と時間がかかった。

1stack build

途中でこのようなエラーがでて build に失敗した。

1configure: error: C compiler cannot create executables
2See 'config.log' for more details

config.log を見ると

1/Users/kazzix/.stack/programs/aarch64-osx/ghc-9.2.5.temp/ghc-9.2.5-aarch64-apple-darwin/configure: line 4392: /usr/local/opt/llvm//bin/clang: No such file or directory

と書いてあり、clang の path がおかしそうなのでCC=/usr/bin/clangを渡して一旦解決。(根本的に解決したいが)

その後に

1stack exec day1-exe

を実行すると

1someFunc

が標準出力に表示された。Hello Haskell !!

コードを書く

適当にググりながらガチャガチャやってたら以外と簡単に書けたが、<-letの違いもわからないし、 Monad がどうたらでエラーが出たときは、さっぱりわからんけど書き換えたら消えた状態(?)なので、どうにかしたい。 あとはコードも手続き風味のコードになってしまったので、関数型風味にしたい。

 1import Text.Read ( readMaybe )
 2import Data.List ( groupBy )
 3import Data.Maybe ( isJust, catMaybes )
 4
 5main :: IO ()
 6main = do
 7  s <- readFile "input.txt"
 8  let parsed = parseStr s
 9  let summed = map sum parsed
10  let m =  maximum summed
11  print m
12
13parseStr :: String -> [[Int]]
14parseStr s = do
15  let splitInput = lines s
16  let parsed = map parseLine splitInput
17  let grouped = groupBy shouldBeGrouped parsed
18  let unwrapped = map catMaybes grouped
19  filter (not . null) unwrapped
20
21parseLine :: String -> Maybe Int
22parseLine = readMaybe
23
24shouldBeGrouped :: Maybe Int -> Maybe Int -> Bool
25shouldBeGrouped a b = isJust a && isJust b

Day 1 Part 2

リファクタもした。

 1import Text.Read ( readMaybe )
 2import Data.List ( groupBy, sort )
 3import Data.Maybe ( isJust, catMaybes )
 4
 5main :: IO ()
 6main = do
 7  s <- readFile "day1.txt"
 8  let ls = lines s
 9  let maybes = readMaybe <$> ls :: [Maybe Int]
10  let calories = map (sum . catMaybes) $ groupBy isJusts maybes
11  let top3 = take 3 $ reverse $ sort calories
12  print $ sum top3
13  where
14    isJusts a b = isJust a && isJust b

Day 2

今度はじゃんけん大会らしい。解く時に書いたメモをそのまま貼る

 1引き分けはそのまま終わる
 2暗号化された strategy guide をもらった。
 31st column が相手が出すやつ 2nd column は...
 4
 5X Rock 1
 6Y Paper 2
 7Z Scissors 3
 8
 9X < Y
10Y < Z
11Z < X
12
13lose 0
14draw 3
15win 6
16
17買った時に ↑ を加算し、その合計が一番高い人が勝利

これは最終的なソースコード。Part2 が Part1 をほぼ含んでいるので Part2 のもの。 Shape に対する Ord の実装をもっとかっこよくかけそうな気はするけれど軽く調べてわからなかったのでこのままにした。 最近はプログラムを書く早さも大事だと思うようになった。

 1import Data.Foldable (find)
 2
 3data Shape = Rock | Paper | Scissors deriving (Show, Eq)
 4
 5instance Ord Shape where
 6  compare Rock Paper = LT
 7  compare Rock Scissors = GT
 8  compare Paper Scissors = LT
 9  compare Paper Rock = GT
10  compare Scissors Rock = LT
11  compare Scissors Paper = GT
12  compare _ _ = EQ
13
14shapes :: [Shape]
15shapes = [Rock, Paper, Scissors]
16
17data Result = Lose | Draw | Win deriving (Enum, Show, Eq)
18
19scoreShape :: Shape -> Int
20scoreShape Rock = 1
21scoreShape Paper = 2
22scoreShape Scissors = 3
23
24scoreResult :: Result -> Int
25scoreResult Lose = 0
26scoreResult Draw = 3
27scoreResult Win = 6
28
29myShape :: Shape -> Result -> Shape
30myShape his r = do
31  let cmp = case r of
32        Lose -> (< his)
33        Draw -> (his ==)
34        Win -> (his <)
35
36  case find cmp shapes of
37    Just s -> s
38    Nothing -> error "unreachable"
39
40readShape :: Char -> Shape
41readShape 'A' = Rock
42readShape 'B' = Paper
43readShape 'C' = Scissors
44readShape _ = error "unexpected"
45
46readResult :: Char -> Result
47readResult 'X' = Lose
48readResult 'Y' = Draw
49readResult 'Z' = Win
50readResult _ = error "unexpected"
51
52readLine :: [Char] -> (Shape, Result)
53readLine i = do
54  let his = readShape $ head i
55  let r = readResult $ i !! 2
56  (his, r)
57
58score :: Shape -> Result -> Int
59score his r = do
60  let m = myShape his r
61  scoreShape m + scoreResult r
62
63main :: IO ()
64main = do
65  ls <- lines <$> readFile "day2.txt"
66  let scores = uncurry score . readLine <$> ls
67  print $ sum scores

Day 3

part2 を分けるのがめんどくさいのと、大きくかわらないので Part1 のソースコードをそのまま改造して Part2 を解くようになった。そのため、この先はすべて Part2 を解くプログラムのソースコードになる。
まずはメモを貼る。

1リュックサックに入った文字列のうち前半と後半で重なるやつを見つける
2
3> To help prioritize item rearrangement, every item type can be converted to a priority:
4
5> Lowercase item types a through z have priorities 1 through 26.
6> Uppercase item types A through Z have priorities 27 through 52.
7
8> What is the sum of the priorities of those item types?

これがソースコード。

 1import Data.Char (isLower, isUpper, ord)
 2import Data.List (intersect)
 3import Data.List.Split
 4
 5main :: IO ()
 6main = do
 7  ls <- lines <$> readFile "day3.txt"
 8  let byGroups = chunksOf 3 ls
 9  let dupPriorities = itemToPriority . findDup <$> byGroups
10  print $ sum dupPriorities
11
12findDup :: [String] -> Char
13findDup (s : ss) = head $ foldr intersect s ss
14findDup _ = error "unexpected"
15
16itemToPriority :: Char -> Int
17itemToPriority c
18  | isLower c = ord c - 96
19  | isUpper c = ord c - 64 + 26
20  | otherwise = error "unexpected"

Day 4

割と慣れてきた気がする。

 1import Data.Char (isDigit)
 2
 3data Range = Range Int Int
 4
 5main :: IO ()
 6main = do
 7  ls <- lines <$> readFile "day4.txt"
 8  let pairs = uncurry overlapsAnother . parseLine <$> ls
 9  print $ length $ filter (== True) pairs
10
11parseLine :: String -> (Range, Range)
12parseLine i = do
13  let (ll, llRest) = takeDigits i
14  let (lr, lrRest) = takeDigits (drop 1 llRest)
15  let (rl, rlRest) = takeDigits (drop 1 lrRest)
16  let (rr, _) = takeDigits (drop 1 rlRest)
17  (Range ll lr, Range rl rr)
18
19takeDigits :: String -> (Int, String)
20takeDigits i = do
21  let ds = takeWhile isDigit i
22  let rest = drop (length ds) i
23  (read ds, rest)
24
25overlapsAnother :: Range -> Range -> Bool
26overlapsAnother (Range ll lr) (Range rl rr)
27  | ll <= rl && rl <= lr = True
28  | ll <= rr && rr <= lr = True
29  | rl <= ll && ll <= rr = True
30  | rl <= lr && lr <= rr = True
31  | otherwise = False

終わりに

ただこのまま書きつづけてもだるいし、全て終わるまで記事を公開できないので、ここまででこの記事は終わり。 実は書いてから公開まで数週間放置していたので、今はもっと理解できている。 Haskell は書いていて楽しいのでもっと触っていきたい。Rust ぐらい慣れられたらいいのだけど、そこまでいけるかな。 最近は Scala もかきたいのでバランスをみて両方触っていけたらと思っている。

Haskell と全く関係ないが、 Avishai Cohen - Samuel を久しぶりに聞いている。好き。