Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scalaのタプルまわりのわだかまり

フォロワーがHaskellの問題で困っていて、助けてくれ〜!! と言われたのだが、 当方Haskellが母語ではないのでハスケルヨクワカリマセン状態になってしまった。というわけでScalaでお答えすることにした。

設問

文字列のペアが、以下に示す全ての条件を満たしているときはtrueを、そうでないならfalseを返せ。ただし入力はペアのリストで与えられる。

  1. 文字列長が等しい。
  2. 文字列のうち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を用いて実現するほうが一般的であるし、curryuncurryによって両者の行き来も容易だ:

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つであるという見え方を前提に置くことで、様々な概念をシンプルに置き換えられることがあるからだ。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?