HaskellでAOJの問題を解いた(ITP1_3~4)
AOJ の Introduction to Programming を解いたときのメモです。
今回解いた問題
- ITP1_3
- ITP1_4
3_A: Print Many Hello World
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_3_A
こちら に書きました。
3_B: Print Test Cases
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_3_B
import qualified Control.Monad as Monad main = loopPutCase 1 loopPutCase :: Int -> IO () loopPutCase i = do x <- getLine Monad.when (read x /= 0) $ do putStrLn $ "Case " ++ show i ++ ": " ++ x loopPutCase $ i + 1
複数の入力データが与えられる問題が初登場しました。
はじめは Writer や State モナドを使ってカウントの状態を管理しないといけないのでは?と考えて色々試行錯誤をしてみましたが、最終的に引数にカウントを取る関数を再帰させる形に落ち着きました。
3_C: Swapping Two Numbers
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_3_C
import Control.Applicative import qualified Control.Monad as Monad main = loopPutNumbers loopPutNumbers :: IO () loopPutNumbers = do [x,y] <- map (read :: String -> Int) . words <$> getLine Monad.when (x /= 0 || y /= 0) $ do putStrLn $ if (x < y) then unwords $ show <$> [x,y] else unwords $ show <$> [y,x] loopPutNumbers
リストもモナドなので <$>
が適用できます。とは言っても getLine で受け取っている IO () も中身は String で同じリストモナドなので今更なのですが。
今回は show <$> [x,y]
と書きましたが、IO () に普通の関数を適用したい場合は <$>
を使えばよさそうですが、リストの場合は map show
とも置き換えられるので好みの問題かもしれません。
3_D: How Many Divisors?
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_3_D
import Control.Applicative main = do [a,b,c] <- map (read :: String -> Int) . words <$> getLine print $ length $ findDivisors a b c findDivisors :: Int -> Int -> Int -> [Int] findDivisors a b c = [n | n <- [a..b], c `mod` n == 0]
問題文から即座に集合の問題だと判断し、リスト内包表記で解いてみました。このような数学らしい問題を解くのに便利な構文があるのも Haskell の強みですね。
4_A: A / B Problem
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_4_A
import Control.Applicative import Data.Fixed main = do [a,b] <- map (read :: String -> Int) . words <$> getLine putStrLn $ show (a `div` b) ++ " " ++ show (a `mod` b) ++ " " ++ show ((realToFrac a) / (realToFrac b) :: Fixed E9)
簡単な除算と剰余演算かと思っていましたが、浮動小数点数の扱いだけは少し悩みました。
/
演算子を使えば除算はできますが、それだけでは期待する結果になりません。例えば、 3 / 2
には 1.50000
という結果が期待されるのですが、実際にはこうならず 1.5
が返ってきます。
この問題を解決するために Data.Fixed というモジュールを使います。このモジュールが提供している型コンストラクタ Fixed を使うことで任意の精度の小数を扱えるようになります。今回は問題文より0.00001以下の誤差が認められているので桁数としては E9
で十分そうです( 10^-9 = .000000001
)。 Fixed 型コンストラクタを使うためには Fractional 型クラスのインスタンスである必要があるので Int を realToFrac
で変換しています。
https://hackage.haskell.org/package/base-4.9.0.0/docs/src/Data.Fixed.html#line-199
4_B: Circle
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_4_B
import Control.Applicative import Data.Fixed main = do r <- (read :: String -> Double) <$> getLine putStrLn $ unwords $ show . toFixedE9 <$> [(pi*r^2), (2*pi*r)] toFixedE9 :: Double -> Fixed E9 toFixedE9 r = realToFrac r :: Fixed E9
Aと同じく浮動小数点数を扱う必要があるので Data.Fixed を使っています。変換処理の記述が長くなって読みづらくなりそうだったので関数に切り出しました。
4_C: Simple Calculator
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_4_C
import Control.Applicative import qualified Control.Monad as Monad main = calculator calculator :: IO () calculator = do [a, op, b] <- words <$> getLine Monad.when (op /= "?") $ do print $ calculate (read a) op (read b) calculator calculate :: Int -> String -> Int -> Int calculate a op b = interpret op where interpret "+" = a + b interpret "-" = a - b interpret "*" = a * b interpret "/" = a `div` b
引数で与えられた演算子の文字列をパターンマッチで評価している部分は、
calculate a "+" b = a + b calculate a "-" b = a - b ...
と書いてもよかったのですが、 where キーワードで一時関数を作ってそこでパターンマッチさせるようにしました。個人的にパターンマッチさせたい値が1つだけの場合や、パターンマッチによる分岐が多い場合は、 where キーワードを使った方が見やすい気がします。
4_D: Min, Max and Sum
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_4_D
import Control.Applicative import qualified Control.Monad as Monad main = do getLine xs <- map (read :: String -> Int) . words <$> getLine putStrLn $ unwords $ show <$> Monad.sequence [minimum, maximum, sum] xs
同じ値に対して複数の関数を適用したいときは sequence
が便利です。似たような関数に sequenceA
というのがありますが、モナドを取るかアプリカティブファンクターを取るかの違いしかなく、今回はリストを取るのでどちらでもよさそうです。
参考文献
Haskellで空白区切りの入力を受け取る
前回、AOJ の問題をいくつか解いたときに空白区切りの入力の扱いに悩んでいましたが、前回より良い方法が見つかったのでメモしておきます。
<$> を使おう
入力として2つの整数が空白区切りで渡されるとき、それぞれの整数を a, b という変数で取りたいとします。すると、次のようなコードになります。
(例: "123 456")
Before
l <- getLine let (a:b:[]) = map read $ words l :: [Int]
After
import Control.Applicative [a, b] <- map read . words <$> getLine
まず <-
の右側から見ていきましょう。
Before では、 getLine したものを一度 l に束縛し、それから words , map read の順で関数を適用することで [Int] 型の値を取ることができました。
それを After では、 <$>
を使うことで、一時的な変数に束縛することなく words , map read を適用できます。
一方 <-
の左側ですが、これは事前に与えられる数が分かっているので素直に2つの要素を持つリストに束縛しました。 (x:xs)
のような文法はパターンマッチで使う方が一般的でしょう(むしろ変数に束縛するのに有用なケースってあるのだろうか)。
<$> とは
<$>
は Control.Applicative
と Data.Functor
にある関数で、アプリカティブ値やファンクター値に対して普通の関数を適用する関数です。
*Main Lib> show <$> Just 1 Just "1" *Main Lib> (+10) <$> [1..5] [11,12,13,14,15]
では、 IO ()
に対して適用できるのはなぜかというと、 IO ()
はモナドであり、またアプリカティブ・ファンクターでもあり、ファンクターでもあるからです。これは GHCi の info オプションでも確認できます。アプリカティブファンクターがモナドのスーパークラスになっているので当たり前なのですが...すっかり抜けていました。
*Main Lib> :i IO () instance Monad IO -- Defined in ‘GHC.Base’ instance Functor IO -- Defined in ‘GHC.Base’ instance Applicative IO -- Defined in ‘GHC.Base’ instance Monoid () -- Defined in ‘GHC.Base’
そして、今回のコード map read . words <$> getLine
では何が起きているのかというと、
- getLine が入力を受け取り IO String を返す
<$>
によって IO String から String を取り出し words に適用する(["123", "456"]
)- その後 map read を適用する(
[123, 456]
)
という流れになっています。
型推論ができない場合
<$>
を使うと IO ()
の中身に対して普通の関数を適用できるようになるのですが、その後の map read で型推論ができない場合があります。その場合は、以下のように型注釈を使って明示的に型を教えてあげる必要があります。
[a, b, c] <- map (read :: String -> Int) . words <$> getLine
まとめ
モナドに埋もれがちですが、アプリカティブファンクターやファンクターも重要であることを再認識したのでしっかり復習しようと思いました。また、 read に型注釈を付けるときにはいつも read x :: Int
みたいな使い方しかしてなかったので、関数全体の型注釈ができるのは知りませんでした。
参考文献
Haskellに慣れるためにAOJの問題を解いてみた
すごいHaskellたのしく学ぼう! や 関数プログラミング実践入門 を読んだだけでは経験値が全く足りないので、AOJ (Aizu Online Judge) を使って Haskell を書く練習をすることにしました。
問題セットは色々ありますが、とりあえず Introduction to Programming から始めていくことにしました。解いていく中で思ったことなどを残しておきます。
解いた問題は GitHub にもあげてあります。
今回解いた問題
- ITP1_1_A~D
- ITP1_2_A~D
- ITP1_3_A
X Cubic
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_1_B
main = do x <- getLine print $ powerThree $ read x powerThree :: Int -> Int powerThree = (^3)
getLine を束縛すると文字列が取れるので、 read で変換してから3乗する関数に渡しました。 それと Int 型の結果は putStrLn ではなく print で出力できるということも忘れていました...(エラーになって気がついた)。 GHCi も出力には print を使っていましたね。
Rectangle
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_1_C
main = do l <- getLine let (a:b:[]) = map read $ words l :: [Int] x = area a b y = circumference a b putStrLn $ show x ++ " " ++ show y area :: Int -> Int -> Int area a b = a * b circumference :: Int -> Int -> Int circumference a b = (a + b) * 2
入力で受け取る空白区切りの文字列をどうやって分割しようかとても悩みました。文字列を分割する関数が Data.Text v:splitOn にありましたが、なるべく他のパッケージを使わないやり方を探してみたかったのです。それと (a:_:b) <- getLine
みたいな束縛のやり方もありますが、 Char を Int に変換するのが面倒だったので止めました。
Watch
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_1_D
main = do s <- getLine putStrLn $ toTime (read s :: Int) 2 toTime :: Int -> Int -> String toTime s 0 = show s toTime s n = show divSecond ++ ":" ++ toTime modSecond (n-1) where divSecond = s `div` (60 ^ n) modSecond = s `mod` (60 ^ n)
問題をみたときにこれは再帰関数で実装できそうと思ったので頑張りました。変換処理のための関数を書いていると Int や String を行ったり来たりするのつらいなあと感じました。
Small, Large, or Equal
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_2_A
main = do l <- getLine let (a:b:[]) = map read $ words l :: [Int] putStrLn $ a `compare'` b compare' :: Int -> Int -> String compare' a b | ordering == LT = "a < b" | ordering == EQ = "a == b" | ordering == GT = "a > b" where ordering = a `compare` b
Ordering の LT, EQ, GT の意味をすぐに忘れてしまうのでメモ。
- A is Less Than B
- A is EQual to B
- A is Greter Than B
文字列でも比較できるからといって文字列比較にしてみたら、 -20 -10
のような入力が来たときに期待する結果にならないことが分かり、入力として整数が与えられるという問題の主旨は履き違えてはいけないなと思いました(当たり前ですが)。
Sorting Three Numbers
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_2_C
import qualified Data.List as List main = do l <- getLine let xs = map read $ words l :: [Int] sxs = map show $ List.sort xs putStrLn $ unwords sxs
ソート問題ということでしたが速度は気にしてなかったので無難に Data.List v:sort を使いました。
パッケージをインポートする際に関数名の衝突を防ぐために qualified というものがあるのですが、個人的には関数名の衝突に限らず付けておいたほうが分かりやすいように思いました。付けないとどのパッケージの関数か分からないので。
Print Many Hello World
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ITP1_3_A
import qualified Data.Function as Function import qualified Control.Monad as Monad main = do flip Function.fix 0 $ \loop i -> Monad.when (i < 1000) $ do putStrLn "Hello World" loop $ i + 1
すごいH本だとループに関する記述は when , forever 関数の説明くらいだった気がするので、他にどんなものがあるのか調べてみました。すると fix という関数を見つかり、この関数を使うと再帰関数を使うことなく再帰を実現することができます。これは fix 関数が、関数の不動点( f a == a
を満たすような値 a のこと)を導くようになっているためです。この辺りの数学の話になってくると半分くらいしか理解できなくなってきます...。
まとめ
いざ問題を解いてみるとなると簡単な問題でも意外と手は動かないもので、まず入力を関数が処理できるような型に変換するにはどうすればいいんだっけといったところから始まり、問題の核となる処理は関数に切り出したほうがいいのか否か、関数の引数の型は入力の型に合わせて String の方がいいのかなど、色々悩みながら書いていました。それでも、解けてACをもらえたときは嬉しいもので、競技プログラミングの問題をやっていて久しぶりに楽しいなと思えました。
引き続き、AOJ の問題を解いていって少しでも Haskell に慣れていきたいです。
参考文献
『コンピューターで「脳」がつくれるか』を読んだ
発売前から大人気だった kazoo04 さんの『コンピューターで「脳」がつくれるか』を読みました。
元々人口知能には興味があったのですが、書店で目にする人口知能を扱った書籍は自分にとっては難しいものばかりで、 集合知プログラミング に手を出してみるもすぐに挫折してしまいました。読んでいてなるほど面白いなと思えたのは 『マッチ箱の脳(AI) - 使える人口知能のお話』 くらいでした。それが2013年の話。
それから時は経ち、マッチ箱の脳みたいな読みやすい書籍はないかなと思っていたところ、かずー氏さんが人口知能に関する本を出版するというツイートを見かけて、即買いしました。
どんな本?
人口知能の本です。ざっくり以下のような内容でした。
- 人口知能と一口に言っても、実は特化型AIと汎用AIという分類ができ、それぞれどう違うのか
- そもそも知能を支える脳って一体どうゆう仕組みなのか
- 特化型AIはどのようにして実現しているのか、機械学習をもとにしたアルゴリズムの解説
- 脳の仕組みや特化型AIのアルゴリズムを交えながら、汎用AIはどうすれば実現できそうなのか
- 汎用AIが実現したときに何が起こるのかなど、未来の話について
著者のかずー氏さん自身も本書のFAQをまとめているので、興味を持った方はそちらを一読するのがよいかと思います。
感想
人口知能の本なので機械学習などのアルゴリズムが分かりやすく解説されているのかなと思っていたのですが、まさか人間の脳の働きについても解説してるとは思いませんでした。けれど、汎用AIを実現する上で人間の脳の仕組みを解き明かすことは重要であることが分かりましたし、未だ解明されていない部分の多い人間の脳は改めてハイテクなんだなあと感じられる内容でした。
さらに、以下の点が本書を読みやすくかつ面白くしているのだと思いました。
- 数式が1つも出てこない
- 未来の話をしてくれる
数式が1つも出てこない
人口知能を扱った書籍はだいたい大学数学レベル以上の数式が載っており(個人的所感です)、パラパラと読むだけでも「うっ頭が...」となってしまうのですが、本書は自分のような方でもすんなりと読むことができます。読む上で、数学と向き合う心構えが必要ないというのはとても安心させられました。
未来の話をしてくれる
実際に汎用AIが実現すると世界がどう変わるのかという疑問にも答えてくれています。SFでいうAIによる人類支配の可能性の話から、雇用問題のような人間との付き合い方の話など、汎用AIと人間の未来についての話で締めてくれるのは個人的にとてもワクワクさせられました。
まとめ
本書でもはじめに触れている通り、人口知能入門の最初の一冊に最適な書籍でした。かずー氏さん本当にすごい...。
自分が冒頭で触れた 『マッチ箱の脳 - 使える人口知能のお話』 は、特化型AIを実現しているアルゴリズム(遺伝的アルゴリズムやニュートラルネットワークなど)についてイラストを入れながら丁寧に説明している書籍なので、本書を読んで特化型AIに興味が出てきた人はこちらもおすすめです。
けれど、汎用AIについてさらに詳しく知りたくなった方は次に何を読めばいいんだろう...。教えてかずー氏さん!
その後
かずー氏さんがリプライで『考える脳 考えるコンピューター』という書籍を教えてくださったので、次はこれを読もうと思います。ありがとうございました!
@ku00_ ご感想ありがとうございます。次の本としては、「考える脳 考えるコンピューター」はどうでしょう?
— かずー (@kazoo04) October 2, 2016
- 作者: ジェフ・ホーキンス,サンドラ・ブレイクスリー,伊藤文英
- 出版社/メーカー: ランダムハウス講談社
- 発売日: 2005/03/24
- メディア: 単行本
- 購入: 15人 クリック: 141回
- この商品を含むブログ (107件) を見る
Yesodに入門しようとして一番始めにyesod-binを入れて失敗した話
Yesod に入門をしようとして 公式サイト の手順を無視したら失敗した話です。
何をしたのか
Yesod を使うために必要なパッケージをインストールするところから始めました。
$ stack install yesod-bin cabal-install --install-ghc ... Copied executables to /Users/hoge/.local/bin: - cabal - yesod - yesod-ar-wrapper - yesod-ghc-wrapper - yesod-ld-wrapper
yesod コマンドを使えるように .local/bin
のパスを通しておきます。
.zshrc
export PATH="$PATH:$HOME/.local/bin"
あとはプロジェクトを作成してビルドしました。ビルドが完了したら stack exec -- yesod devel
でサーバを起動することができるはずでした。
$ stack new hello-yesod yesod-sqlite $ cd hello-yesod $ stack build $ stack exec -- yesod devel Yesod devel server. Type 'quit' to quit Application can be accessed at: ... Resolving dependencies... Configuring hello-yesod-0.0.0... ERROR: Yesod has been compiled with a different GHC version, please reinstall yesod-bin yesod: ExitFailure 1
失敗しました(´・_・`)
なぜ失敗したのか
ブログタイトルにも書きましたが、 stack install yesod-bin
を一番始めにやったのがよくなかったようです。
stack exec -- yesod devel
を実行したときに出たエラーメッセージを見てみると
ERROR: Yesod has been compiled with a different GHC version, please reinstall yesod-bin
と書かれています。すでに yesod-bin がプロジェクトで使用されている GHC とは違うバージョンでビルドされていて、それが原因のようです。
実は stack install yesod-bin
を実行したログの最初のほうで、グローバルプロジェクトの stack.yaml
が読み込まれていて、そこに書かれた resolver でビルドされていたようです。
Run from outside a project, using implicit global project config Using latest snapshot resolver: lts-7.0 Writing implicit global project config file to: /Users/hoge/.stack/global-project/stack.yaml
確認してみると確かにバージョンが違いました。
~/.stack/global-project/stack.yaml
... resolver: lts-7.0
hello-yesod/stack.yaml
... resolver: lts-6.18 ...
解決策
再ビルドしてみる
再インストールするのは時間がかかるので、とりあえずプロジェクトのディレクトリまで移動してから再ビルドしてみます。
$ ls .stack/snapshots/x86_64-osx/lts-7.0/8.0.1/installed-packages . .. cabal-install-1.24.0.0 yesod-bin-1.4.18.3 $ stack build yesod-bin-1.4.18.3
ビルドが終わったところで再度サーバを起動してみます。
$ stack exec -- yesod devel Yesod devel server. Type 'quit' to quit Application can be accessed at: ... Resolving dependencies... Configuring hello-yesod-0.0.0... Forcing recompile for ./Model.hs because of config/models ... Rebuilding application... (using cabal) Starting development server... <command line>: cannot satisfy -package-key main (use -v for more information) Exit code: ExitFailure 1
ダメでした(´・_・`)
インストールし直す
仕方ないので一度インストールしたパッケージたちを消してから、公式サイト の手順に沿ってインストールし直します。
(念のため cabel も一緒に消しておきます)
$ rm -rf ~/.stack/snapshots/x86_64-osx/lts-7.0 $ cd ~/.local/bin $ rm cabel $ rm yesod $ rm yesod-*
先にプロジェクトを作成してから yesod-bin をインストールします。
$ stack new hello-yesod yesod-sqlite $ cd hello-yesod $ stack build yesod-bin cabal-install --install-ghc $ stack build
ビルドが終わったらサーバを起動してみます。
$ stack exec -- yesod devel Yesod devel server. Type 'quit' to quit ... Rebuilding application... (using cabal) Starting development server... Starting devel application Migrating: CREATE TABLE "user"("id" INTEGER PRIMARY KEY,"ident" VARCHAR NOT NULL,"password" VARCHAR NULL,CONSTRAINT "unique_user" UNIQUE ("ident")) ...
先ほどとは違い DB のマイグレーションまで実行されました。そして localhost:3000
を開いてみるとようやく Scaffold されたサイトを見ることができました。
まとめ
公式サイトの手順には基本従いましょう(´・_・`)
参考文献
Stack を使って Haskell の Hello World をやってみる
以前 Stack を使って すごいH本 を学習したのですが、それっきり全く Haskell に触れていなかったのでリハビリがてら、 Stack を導入して Hello World を出力するまでの方法について書いておきます。
Stack のインストール
Mac の場合は Homebrew を使ってインストールできます。
brew install haskell-stack
プロジェクトの作成とビルド
公式の Quick Start Guide に書いてある通りにやりましょう。
Quick Start Guide — stack documentation
stack new my-project cd my-project stack setup stack build
Hello World!
次は、プロジェクトの中身を少し修正しておなじみの Hello World を出力してみましょう。
app/Main.hs
module Main where import Lib main :: IO () main = someFunc
app/Main.hs
がメインのパッケージです。
Hello World を出力するためには、この中の main
関数を修正すればよさそうですが、コードを見た限りだと main
は someFunc
という関数を指しているだけです。
someFunc
の実体は Lib
パッケージ内にあります。 import Lib
で Lib
パッケージをインポートしていてその関数を使っているわけです。
インポートされているパッケージはデフォルトでは src
以下に置かれているので見てみましょう。
src/Lib.hs
module Lib ( someFunc ) where someFunc :: IO () someFunc = putStrLn "someFunc"
src/Lib.hs
の最後の2行が someFunc
の実体になります。 someFunc = putStrLn "someFunc"
を someFunc = putStrLn "Hello World"
という風に修正します。
そして stack runghc
で main
関数を実行できます。
( stack exec my-project-exe
でも同様の結果が得られます)
$ stack runghc app/Main.hs Hello World
以上です。簡単でしたね。
おまけ
先ほど実行した app/Main.hs
、実は stack ghci
でも実行できます。
$ stack ghci The following GHC options are incompatible with GHCi and have not been passed to it: -threaded Using main module: 1. Package 'my-project' component exe:my-project-exe with main-is file: /my-project/app/Main.hs Configuring GHCi with the following packages: my-project GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help [1 of 1] Compiling Lib ( /my-project/src/Lib.hs, interpreted ) Ok, modules loaded: Lib. [2 of 2] Compiling Main ( /my-project/app/Main.hs, interpreted ) Ok, modules loaded: Lib, Main. *Main Lib> main Hello World
GHCi はすごいH本を学習している方はおなじみですね。
なので、これから Stack を使ってすごいH本を学習しようと考えている人は、
- 作成したパッケージは
src
以下に置く stack ghci
で GHCi を起動した状態で、パッケージをロードして関数を実行するmain
関数のような I/O アクションを実行する場合は、stack runghc src/MyPackage.hs
を使う
というスタイルがよいかもしれません(自分はこうやって学習していました)。
例えば src/Baby.hs
内の doubleMe
という関数が使いたい場合は、次のように :l
でパッケージのロードをすることで使えるようになります。
$ stack ghci ... *Main Lib> :l src/Baby.hs [1 of 1] Compiling Main ( src/Baby.hs, interpreted ) Ok, modules loaded: Main. *Main> doubleMe 9 18
参考文献
『Rubyによるデザインパターン』を読んだ話
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 13人 クリック: 220回
- この商品を含むブログ (67件) を見る
ピアソン・エデュケーションの『Rubyによるデザインパターン』を読みました。
Webエンジニアとして働いていながら、今までデザインパターンというものを知らずにきてしまっていて、先日チームの先輩エンジニアからコードレビューで「ここはアダプタパターンで書いたほうがすっきりするよ。」と指摘されなんだそれは???となったのがきっかけでした。そんなデザインパターン初心者の自分にその先輩エンジニアが貸して下さったのが本書でした。
どんな本?
内容としては、ギャング・オブ・フォー本『オブジェクト指向における再利用のためのデザインパターン』に登場する23個のデザインパターンのうち14個について、 Ruby のコードを例に解説してくれています。はじめはデザインパターンと聞いて、勝手に難解なイメージを抱いていましたが、本書は比較的 Ruby 経験が浅い人でも読みやすい内容になっている印象を受けました。
また、2009年に初版が発売されておりだいぶ古い書籍です。さらに本書の出版社が書籍から撤退したために絶版状態となり、入手が困難な状況です。 自分はお借りした本書を読了後に、Amazonの中古で定価+¥2000くらいで購入しました。
感想
当初知りたかったアダプタ( Adapter )パターンについても、そこまで多くのページ数は割かれていませんが、そのパターンがどんなものかを知るには十分な量でした。章によっては、デザインパターンの解説と共に、実際にそのパターンが利用されているプロダクトの例をあげていたのも良かったです。
今まで漠然と書かれているように見えていたコードも実は規則性が存在するものがあるという発見は、自分がコードを見る時の新たな視点をもらったような気がしました。
また、本書を読み進めていくなかで、このデザインパターンいいな、使ってみたいなと思うことが時々ありましたが、そんなときは本書に書かれていたこの言葉を思い出すようにしています。
デザインパターンの適切な使い方は、今日あなたが持っている問題に対応できるだけの柔軟性をシステムに組み込むことで、それ以上ではありません。 パターンは便利なテクニックですが、それ自身が目的になってはいけません。
- 1.2.5 必要になるまで作るな(p.13)より
「必要になるまで作るな( YAGNI )」というエクストリーム・プログラミングにおける原則があるように、必要以上の機能や設計の柔軟性は、デザインパターンを適用しなかったものと比較したときに大差が無かったり、却ってコードの可読性を下げる恐れがあるので使い所は考えましょう、という本書からの警告も念頭に置きつつ読んでみるとよいかと思います。
読んでみて自分がこのデザインパターンは使うことありそうだなと思ったのは
- Adapter パターン
- Decorator パターン
くらいでした。と言っても Rails を書いていると Observer や Iterator は普段意識せずに使うことができるので、自前で特定のデザインパターンを適用するみたいなケースはあまりないのかもしれません(自分が本書で紹介されているようなデザインパターンを適用するような問題にぶち当たっていないだけかもしれませんが)。
一方で Convention over Configuration (CoC)の章は、普段自分が Rails を書いてあまり意識することのなかったディレクトリ構造やファイル名の規則について、なぜそうなっているかという疑問を解消するのに役立ちました。書かれているコードだけでなくディレクトリ構造やファイルの名称なども含めて1つのデザインパターンなんだというのを再認識させられました。
まとめ
Ruby でデザインパターンを学びたいという人は読むといいかもしれません。定価では買えないので泣きながらAmazonの中古を買うか、偉い人に頼んで買ってもらうかしましょう(´・_・`)