フォロワーがHaskellの問題で困っていて、助けてくれ〜!! と言われたのだが、 当方Haskellが母語ではないのでハスケルヨクワカリマセン状態になってしまった。というわけでScalaでお答えすることにした。
#Haskell でもっとスマートなプログラムを考えて下さる方いらっしゃいませんか……
— 𝓔𝓹𝓸𝓷𝔂𝓶 (@veponym) 2023年6月23日
条件を両方とも満たすか判定するプログラムを作成してください。
満たすならば Yes 、満たさなければ No を、改行区切りで出力
・2 つの文字列は同じ長さである
・2 つの文字列は、ちょうど 1 か所の文字が異なる pic.twitter.com/fpBK4e4PaT
うお〜、Haskellは母語ではなくてあまり書けないのでScalaでしたら力添えできると思います。Scalaで同じような問題を解くので、それを参考にしていただく、というのはどうでしょう。
— めるくん (@windymelt) 2023年6月23日
設問
文字列のペアが、以下に示す全ての条件を満たしているときはtrueを、そうでないならfalseを返せ。ただし入力はペアのリストで与えられる。
- 文字列長が等しい。
- 文字列のうち1文字だけが異なっている。
素朴な回答
素朴に書くとこういう感じになる。Catsを使う必要もない。
def sameLength(s1: String, s2: String): Boolean = s1.length == s2.length def differsExactlyOneChar(s1: String, s2: String): Boolean = (s1 zip s2).filter { case (c1, c2) => c1 != c2 }.size == 1 val xs = List( ("aruru", "araru"), ("aruru", "arara"), ("aruru", "aruru"), ("algo", "all"), ("std", "str") ) def similar(ss: (String, String)) = sameLength.tupled(ss) && differsExactlyOneChar.tupled(ss) xs.map(similar) // => List(true, false, false, false, true)
sameLength
は特筆するべきこともない。differsExactlyOneChar
もセオリー通りで、2つの文字列をzipし、各文字ごとの差異をさぐり、差異がある要素の数が1であればtrueである。ミクロな視点での部品はこれで揃った。
さて、ここで視点を転換する。マクロな視点での部品に着目すると、大きな流れとしては「リストをmap
したら終わり」という体裁にしたい。それが最もシンプルである。この中間を埋める存在として、similar
を実装する。
similar
の仕事も特別複雑なものではない。文字列のペアをタプルとして受け取り、2つの関数にそれぞれ配り、その結果を&&
で合成しているだけである。
あとは与えられた入力xs
に対してsimilar
をmapすれば結果が得られる。
タプルとScala
Scalaは、その言語的出自の影響により、複数引数を渡すこととタプルで引数を渡すこととの間に微妙なわだかまりが残っている。例えば、2引数メソッドdef f(x: Int, y: Int): Int
に2-タプル(42, 43)
を渡すことは、そのままではできない。
def f(x: Int, y: Int): Int = x + y val p = (42, 43) f(p) // => does not compile!!
Haskellにおいて、複数の引数を同時に受け取ることとはタプルを使うことにほかならないので、このようなわだかまりは無い。
f (x, y) = x + y main :: IO () main = do let (x, y) = (42, 43) putStrLn $ show $ f (x, y)
また、Haskellでは複数の引数を受け取るときはカリー化curryingを用いて実現するほうが一般的であるし、curry
とuncurry
によって両者の行き来も容易だ:
f (x, y) = x + y g x y = x + y -- こちらのカリー化した表現が一般的 main :: IO () main = do let (x, y) = (42, 43) putStrLn $ show $ g x y putStrLn $ show $ (curry f) x y
さて、Scalaに話を戻すと、複数引数を受け取るメソッドにタプルを配りたいとか、タプルを受け取るメソッド内でこれを展開したいときなどに、いささか手間がかかる。
def f(x: Int, y: Int): Int = x + y val p = (42, 43) f(p) //does not compile!!!
val p = (42, 43) val xs = Seq(p, p, p, p, p) xs.map { case (x, y) => ... } // caseを使わなければ展開できない
実際、最初の例でも、case
によって書き味が大きく損われている箇所がある:
(s1 zip s2).filter { case (c1, c2) => c1 != c2 }.size == 1
本当はこう書けると一番良い:
(s1 zip s2).filter { _ != _ }.size == 1 // does not compile!!!
Tupleまわりをうまく変換するために、Scalaにはtupled
メソッドが用意されている。tupled
は、複数引数を取るメソッドをタプルを取る形式へと変換する:
def f(x: Int, y: Int): Int = x + y val ff = f.tupled val p = (42, 43) ff(p) // => 85
Catsなどを使った関数型プログラミングを行う場合、タプルになっていたほうが大抵扱いやすい。 引数は常に1つであるという見え方を前提に置くことで、様々な概念をシンプルに置き換えられることがあるからだ。