トイレクラスで見る凝集度と責務

トイレとは何か?

社内向けに凝集度と責務について発表しました。

正直、プレゼン形式での発表は慣れないので全然うまく話せませんでしたが、調べるためのインデックスができればいいくらいの気持ちと、LTにも慣れていきたいなと思ってやってます。

折角なのでブログにも少し残すことにしました。

さて、今回の発表では「トイレ」クラスを例に使いました。どこで見たのか忘れたのですが、「凝集度が低いとは、自分の家のトイレのレバーが100m先の人の家の居間にあるような状態」みたいな例を挙げている方がいて、その印象が強かったからです。

トイレクラスに入る前に先に凝集度と責務について確認します。

凝集度

関連性の強いデータとロジックだけを集めたクラスを凝集度が高いと言います。

現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 増田 亨

個人的にわかりやすいと思ってる、松岡さんの定期ツイートでの説明。

責務

責務は、ソフトウェアオブジェクトについて大雑把に記述したものです。責務には以下の3つの主要な項目が含まれます。

・オブジェクトが行う動作

・オブジェクトが持つ知識

・オブジェクトが他に影響を与える主要な判断

オブジェクトデザイン レベッカ・ワーフすブラック, アラン・マクキーン 4.1 責務とは何か

下記の記事も参考になります。

ドメインオブジェクトの責務について

具体例でイメージを掴む〜トイレクラスの例〜

私が凝集度と責務を意識するようになった例から、トイレクラスを考えます。

トイレは下記のような知識?と動作をもつと考えられます。

  • トイレはタンク(便器中央の水が張っているところ)を持つ。
  • トイレは流す(タンクを空にし、新たな水を張る)ことができる。

低凝集な実装が行われる時は大体、2つ目の流す機能の方が注目されて実装されます。

object HomeHelper {
  // 対象のトイレのタンクを空にする
  def flushToToilet(toilet: Toilet): Unit = {
    toilet.tank.dequeueAll(_ => true)
    toilet.tank.enqueue(Water(1.0))
  }

  // 対象のトイレに何かを put する
  def putSomethingToToilet(something: Any, targetToilet: Toilet): Unit =
    targetToilet.tank.enqueue(something)
}

低凝集な実装でよく見るパターンは上記のような実装ですね。綺麗にデータクラスと、機能クラスが分かれています。最悪です。

上記は水洗トイレではなく、中世の欧州の方の貴族は道端で用を足していたと聞きますがそんな感じです。

まず、人間が床(メモリ)の上に用を足します。

それをなんか清掃作業員みたいな人がトイレって名前のついたバケツを持ってきて回収し、最後川に流すみたいな実装です。つまりこういうこと。

// 人がまず床に便をする
val poops = person.defecation

// 便回収用のバケツ準備する
val poopBucket = Toilet(mutable.Queue(Water(1.0)))

// バケツに便を回収する
HomeHelper.putSomethingToToilet(poops, poopBucket)

// バケツに溜まった便を川に流す。
HomeHelper.flushToilet(poopBucket)</pre>

もはやトイレでもなんでもないただのバケツですね。

次は、責務がおかしいトイレを考えましょう。スライドにも載せているんですが、イラスト屋に責務がおかしいトイレの画像があったのでそれを例に使いましょう。

トイレに本を積む責務を持たせてしまっている

この画像は明らかにトイレの責務がおかしいですね。トイレは便や尿を流すためのものであって、本をstackするのは明らかに責務違反です。ちなみにこの状態をコードで表現すると以下のようになります。

class Toilet {
  private val tank: mutable.Queue[Any] = mutable.Queue(Water(1.0))

  // トイレに本を積む責務を持たせてしまっている
  private val bookShelf: mutable.Stack[Book] = mutable.Stack()

  def stackBook(book: Book): Unit = bookShelf.push(book)

  ...
}

明らかにおかしいですが、現実世界のプロダクトではわかりにくいビジネス上の概念を扱うためによく起こります。

最後、高凝集かつ責務が正しいトイレを見ていきましょう。

class Toilet {
  // トイレは最初水を張っている状態
  private val tank: mutable.Queue[Any] = mutable.Queue(Water(1.0))

  // トイレに(茶色い)何かをputする
  def put(something: Any): Unit = tank.enqueue(something)

  // トイレを流す
  def flush(): Unit = {
    // タンクに入っているものを流し
    tank.dequeueAll(_ => true)
    // 新たな水を追加する
    tank.enqueue(Water(1.0))
  }
}

私たちが普段使っているトイレが表現できていると思います。

これならば、後続の開発者が見たときに「トイレ」がどういうものかを理解できると思います。

Scalaz の Tree で違いがあるかを判定する

※Treeのflattenの結果が同じ値ならば常に同じであることを前提としていて、実際そうなのかをちゃんと確認してないので少し怪しいところあり。

ここ1年くらい木構造に悩まされていますが、scalaz の Tree を使っていて、2つの木に違いが存在するかを判定したい場合にどうするかのメモです。

どう考えたか?

scalaz の Tree の flatten を同じ木構造のデータに実施した際に同じ並び順のListになるならば、一旦Listにして同じindexの要素を比較してあげれば、違いがあるかどうかわかるんじゃない?って単純な発想。

Tree の拡張として作りたいので implicit class で定義する。

implicit class MyTreeOps[A](value: Tree[A]) {
    def isSame(other: Tree[A]): Boolean = {
      val list1 = value.flatten.toList
      val list2 = other.flatten.toList
      list1.zipWithIndex.map { case (v, index) =>
        list2.lift(index) match {
          case Some(value) => value == v
          case None => false
        }
      }.forall(b => b)
    }
  }

自分自身をflattenしたものと、比較対象をflattenしたものに対して、同じ位置にある要素を比較して、全てtrueならば一致しているとみなしています。

これをさらに簡潔に書くと

implicit class MyTreeOps[A](value: Tree[A]) {
    def isSame(other: Tree[A]): Boolean = {
      val list1 = value.flatten.toList
      val list2 = other.flatten.toList
      list1.zipWithIndex.map { case (v, index) =>
        list2.lift(index).contains(v) // 上記パターンマッチと同義の処理
      }.forall(b => b)
    }
  }

のようにも書ける。最初の方が意図が伝わりやすい気もするのでどう書くかは好みで。

実際に試してみる。

import scalaz.Scalaz.ToTreeOps
import scalaz.Tree

object Main {
  def main(args: Array[String]): Unit = {
    println(freeTree isSame freeTree) // true
    println(freeTree isSame freeTree2) // false
  }

  implicit class MyTreeOps[A](value: Tree[A]) {
    def isSame(other: Tree[A]): Boolean = {
      val list1 = value.flatten.toList
      val list2 = other.flatten.toList
      list1.zipWithIndex.map { case (v, index) =>
        list2.lift(index).contains(v)
      }.forall(b => b)
    }
  }

  def freeTree: Tree[Char] =
    'P'.node(
      'O'.node(
        'L'.node('N'.leaf, 'T'.leaf),
        'Y'.node('S'.leaf, 'A'.leaf)),
      'L'.node(
        'W'.node('C'.leaf, 'R'.leaf),
        'A'.node('A'.leaf, 'C'.leaf)))

  def freeTree2: Tree[Char] =
    'P'.node(
      'O'.node(
        'L'.node('N'.leaf, 'T'.leaf),
        'Y'.node('S'.leaf, 'A'.leaf)),
      'L'.node(
        'W'.node('C'.leaf, 'R'.leaf),
        'A'.leaf
      )
    )
}

Scalaでの例外処理メモ

例外についてものすごく参考になった記事と、個人的メモ。

参考になった記事

Scala界隈では有名ながくぞさんのSlideですね。私はこれを読んで初めて例外が何か理解できた気がしました。

他にもスライドはあります。

https://gakuzzzz.github.io/slides/

Javaにおける例外

Scalaにおける例外の立ち位置や、どう扱うべきかを理解するにはJavaで例外をどう扱うかを先に理解するのがわかりやすいです。

Javaの例外には3種類あります。

  • 致命的なエラー: Error
  • 非検査例外:RuntimeException
  • 検査例外:Exception

このうち、致命的な例外と非検査例外については捕捉せず、検査例外に対してのみ呼び出し側でハンドリングをします。

エラーの種類ハンドリング
致命的なエラー(ErrorNoOutOfMemoryError, StackOverflowError など。
非検査例外(RuntimeExceptionNoIllegalArgumentException など。バグ起因系。
検査例外(ExceptionYes業務例外など。
エラーの種類とハンドリングすべきかどうか

基本的に、検査例外以外はハンドリングする必要がありません。※ハンドリングすべきでない。

Scalaにおける例外

Scalaにおいては、検査例外をなくし、例外は全て致命的なエラーもしくは、非検査例外のハンドリングしてはならないもの、しなくて良いものだけになりました。

  • 致命的な例外
  • 非検査例外

のみ。

検査例外をどう扱うか?

Scalaで検査例外を扱う際は型で表現します。TryEither などですね。Option もそうです。この辺りの詳しい説明はドワンゴの新卒研修資料がわかりやすいと思います。

scala-text/エラー処理

これにより、型で利用者側が明確に例外処理をいけないんだなと分かりますし、Scalaのパターンマッチを使ってあげれば網羅性検査も可能です。

実装アイデア

実際にどういうふうに書くかの実装アイデアを考えます。

下記のようなシチュエーションを考えます。

インターネット上でプリペイド式の書籍を購入できるサービスを開発しています。

今現在、ある書籍(Book)を購入する処理を書いています。

この処理には次の業務例外が発生すると想定されています。

  • チャージされているお金が足りない場合
  • 倉庫に在庫が存在しない場合

成功時には、荷物追跡IDを返すことにします。

これをScalaで書く場合を考えます。

// ISBNコード
case class ISBN(value: String)
// 価格
case class Price(value: Int)
// 本
case class Book(isbn: ISBN, title: String, price: Price)
// 追跡番号
case class TrackingNumber(value: String)

trait BookPurchaseService { import BookPurchaseService._ // 購入処理 def purchase(book: Book): Either[PurchaseError,TrackingNumber] }

object BookPurchaseService { sealed trait PurchaseError // 在庫が足りない case object NoStock extends PurchaseError // チャージ金額が足りない case class InsufficientChargeAmount(insufficientAmount: Int) extends PurchaseError }

こんな感じにしてあげると、PurchaseErrorsealed trait になっているので、網羅性検査も言語機能側でやってくれて便利になるかなと思います。

書き方のイメージは自分の中では今のところ上記のような感じですが、ここで重要なのは

  • 例外には3種類あって、捕捉すべき例外と、捕捉すべきでない例外がある
  • 捕捉すべき例外はScalaでは値で表現する

の2点かと思います。

【Scala】Option 利用指針

Scalaでよく使うものに Option 型があると思います。Option 型は値が存在するかもしれないことを示す型です。Javaで値がないを表現するためには null を使うのが一般的でした。Java 8からは Optional ができたようですが、古いJavaのプロダクトでは null が使われていたと思います。

この、Option ですが、非常に便利で無闇に多用されているのを見かけます。例えば、

val users: Seq[User] = Seq(User(1, "司馬"), User(2, "光井"), User(3, "北山"))
val maybeUser: Option[User] = users.find(_.id == 1) // 仕様上、絶対存在するはず

このような形です。重要なのは「仕様上、絶対存在するはず」の部分です。仕様上存在するはずですが、find メソッドは Option で返すので、Option のまま後続の処理を続けようとしているのを見かけます。

maybeUser match {
  case Some(user) => // 何らかの処理
  case None       => // 仕様上絶対に起こらないけど不安だから一応書いているハンドリング
}

だったり、雰囲気で

val user = users.find(_.id == 1).getOrElse(User(0, ""))

のように謎の値がデフォルト値として採用されていたりします。私も新卒で入社して、Scala書きはじめた頃はこのような書き方をよくしてしまっていました。しかし、この書き方に大きな問題が3つ存在します。

3つの問題

  1. コードが複雑になる
  2. 不安になる
  3. バグに気づけなくなる

1 コードが複雑になる

これは単純です。Option の値を参照するクライアントは Some or None でハンドリングをしなければなりません。仕様上必ず存在するはずの値をいちいちハンドリングするのは無駄でしかありませんし、コードの可読性を下げるだけです。

2 不安になる

頭の中では、絶対に存在すると分かっていても、値が Option のままだと不安になります。この値は参照しようとしている今この瞬間本当に存在しているのだろうか。ないことはないのだろうかと不安になります。不安は脳のリソースを奪い、生産性を下げます。

3 バグに気づかなくなる

これが最大の問題です。最初から言っている通り、ここで議論しているのは「仕様上必ず存在するはず」の値です。仕様上必ず存在するはずの値が存在しない時、それはバグなので、バグとして気付けなければなりません。つまり、getOrElse やパターンマッチでハンドリングしてしまうと、バグを握りつぶすことになってしまいます。非検査例外を、キャッチして握りつぶしてしまっている感じですね。

どうすべきか

単純です。最初の段階で get してしまいましょう。

val user: User = users.find(_.id == 1).get

これで NoSuchElementException が投げられたら、それはバグです。`users に問題があったことになります。

どういう時に見かけるのか

例に出した、配列の中に仕様上必ず存在しなければならない時もありますが、結構困るのは、RDBのテーブル定義が、値は必ず存在するが、テーブル定義は null になっている(not null になっていない)時です。テーブル定義を変えてしまうのがベストですが、DBスキーマ変更のコミュニケーションコストが高いプロジェクトの場合ですと、気軽に変更できなかったりもします。そういう時は、仕様をよく確認し、スキーマnull だけど実はnull にはならない値の時は迷わず get を使います。

まとめ

  • そもそも Option を気軽に利用しないようにしよう。Option を使うべき時は、仕様上Optional な時だけだ。
  • 安易に getOrElse をしないようにしよう。安易なデフォルト値はバグの見落としに繋がる。
  • 仕様上、必ず存在する値だとわかったら、迷わず getOption から取り出そう。大丈夫、もしエラーが出た時は、バグを発見できたと喜ぼう。

RDBのカラムを安易に nullable にするやつを俺は許せない。

Mac mini m1 買った(届いてない)。選択理由

今更ながら、Mac mini m1 買いました。

動機

単純に、今使っている MacBook Pro 2016 (16GB, 2.9GHz デュアルコア)が開発用途での利用に厳しくなってきたからです。IntelliJを起動するのに数分かかったり、キーボードの入力から反映まで1〜2秒のタイムラグは日常でした。このストレスから、プライベートな開発や色々試したりするのが最近億劫になっていたのです。

結構前からそのような状態だったので、次、Mac BookPro m1 14インチが発表されたら買い換えようと思ってましたが、待てませんでした。。

選定理由

PCを選定する上で、選定する際の基準を考えてみます。私がいつも考えるのは下記です。

今回は、後半2つが悩みどころでした。

Mac or Windows or Linux

まずは王道のこの3択ですね。私個人としては現状Mac 1択なので、特に考慮してはいませんが、それだと脳死Mac選んでるみたいで嫌なので、選択理由を書いておきます。

Linuxでない理由

なんだか、スーパーハカーな感じの人は、Mac使うくらいなら、Linux使えよ。金の無駄だって言ってるイメージがあるんですが、メインPCをLinuxにする理由はないですね。単純に開発用途以外で使いづらいです。世の中、WindowsMac向けにクライアントソフトは開発されているので。実際、一時期黒い画面でなんかすごいことしてそうな人のイメージに憧れて、Ubuntuを使おうとインストールしたこともあったんですが、結局Windowsに戻りましたし、Windowsの方がスムーズに開発できました。(ネット情報が多いので、詰まりにくい)。まあ、普通の人にとってLinuxはサーバー用途です。クライアントPC用途ではない。

Windowsでない理由

特にないです。正直、最近はWindowsでもいいかなと思うことがしばしば。私はゲームはPS4でできれば充分派ですが、Windowsでしかできないゲームを見つけて、それがやりたかった時、Winodowsも持っておくべきか。。と思うことはあります。

まだMacに比べて面倒が多いなとは思いますね。WSLで解消されてきてはいますが。

Macである理由

Apple製品で固めてるからですね。エコシステムに組み込まれてると、製品間の連携が楽です。Amazon EchoもHome pod mini に置き換えたい。

ノート or デスクトップ, 価格

今回の争点です。私は今まで個人のPCとしてはノート型しか買ったことがありませんでした。なんとなく持ち運びできた方がいい気がしましたし、キーボード、マウスともにこだわりがないので、一体になってるノート型はコスパがいいという判断です。ノートを選択・購入していた頃はスペックもそこそこで事足りていたので、マシンスペックで困ることもありませんでした。今使っているMBPなんかは、当時としてはオーバースペックくらいの印象。

しかし、最近は性能面と価格がボトルネックになってきました。最近はIntelliJを複数起動することはいつもですし、dockerで複数システムを起動したり、常時コンパイルとテストを走らせていたい(が、重くなりすぎてできない)といった感じになってきました。そうなると、メモリは足りない、マシンは熱暴走気味で動かなくなる。ファンが頑張ってもすぐ熱は溜まると開発できたものではなくなってました。そうなってくると、もっとハイスペックなマシンが欲しくなります。MBPでそれを実現するとすぐに30万を超えてきます。出費としては痛すぎます。※もっと金持ちならよかったのに。。もっと安く抑えるには必然的にノート型は諦めることになります。

ノートを諦めると、PCを持ち運ぶことはできなくなりますが、私にとってこれは特に問題ありませんでした。実際、外で開発をすることはほとんどありませんでしたし、今はオンラインでのコミュニケーションも主流になってきて、ますますオフでPCを持ち運ぶようなことはなくなってきました。そしたらもう、デスクトップ型で問題ありません。

そうして私は、デスクトップ型のMac miniを選択しました。

結論

MBPは高い。

Mac miniは安い。

外にPC持ち運ばない。

Mac miniでいいじゃん?

外で開発しなきゃならない時は、AWS上とかで開発すればいいんじゃないかって気がしてきている。

Scala で Builder Pattern

ScalaJava like なBuilder Patternを書く機会は自分の場合あまり無い(Static Factoryで事足りることが多い)のですが、少し古いJavaのプロダクトのリファクタをした際にBuilder Patternで生成処理を書き直したので、Scalaの場合はどうなるのか見ていこうと思います。

Generalized Typed Constraint の勉強にも一部なるところもあったりします。

JavaにおけるBuilder Pattern

JavaにおけるBuilder Pattern は GoF と Effective Java が有名かなと思います。Builder Patternの説明としては、Effective Javaの方が使い所とかが個人的には分かりやすい気がします。コードとしても、Effective Javaの書き方の方が、コードが散らばらないので好みですね。※Effective Javaはクラス内に `static class` として、`Builder` クラスを定義するが、GoFの方は、`Person` クラスと別に、`PersonBuilder` のようなクラスを作る。

JavaにおけるBuilder Pattern を使いたくなるときと、BuilderPattern以前の書き方

Builder Patternが使いたくなるのは、必須では無いけれど場合に応じて外から値を渡してオブジェクトを生成したい時、オプショナルパラメータが多い時に使いたくなります。オプショナルパラメータが多い時に選択される伝統的なパターンとして、Effective Javaではテレスコーピング・コンストラクタ(Telescoping Constructor)とJavaBeansパターンが紹介されていました。

テレスコーピング・コンストラクタは、単純にコンストラクタを複数用意するパターンです。

6個のパラメータでこれなので、コンストラクタがいくつあっても足りないですし、上から順番にしか対応できてないので、例えば、`carbohydrate` は設定したいけど、他のパラメータはデフォルトパラメータで良いとかには対応できません。

私がこの業界に入った時にはすでにJavaBeansパターンは廃れてましたが、古いプロダクトだとやはりよく使われています。JavaBeans パターンは setter を使って行うパターンです。そもそも不変でないので、バグが入り込みやすいですし、Scalaではまず出会わないですね。でもやっぱり古いJavaのプロダクトだとよく見ます。setterを排除したい時に、Builder Patternに置き換えるのはリファクタとしてはやりやすいなと思います。

Scalaを使っていると、ほぼ直面しない、値がミュータブルな状態なってしまうのがJavaBeansパターンの最悪の欠点ですね。最新のJavaは知らないですが、基本的に値がミュータブルになってるJavaといえど、イミュータブルの方が扱いやすいことには変わりありません。しかし、JavaBeansパターンはテレスコーピングパターンよりも柔軟です。値変えたいもののsetterのみを呼び出せば良いわけなので。

Builder Pattern (Java

テレスコーピングパターンも、JavaBeansパターンもどちらも使いづらい。そこで使われるのがBuilder Patternです。(Effective Java ver.)

呼び出しのところを見ればわかるように、Builderからメソッドチェーンのように生成処理が書けます。

ScalaにおけるBuilder Pattern

Javaの方を見てわかるように、基本的にはオプショナルパラメータはデフォルト値で良くて、時々オプショナルパラメータの値を外部から渡したい。そしてそのようなオプショナルパラメータが多い時にBuilder Patternは活躍します。

case class のデフォルトパラメータを使う

Scalaでデフォルトパラメータを使う場合は、すごく簡単にかけます。

JavaのBuilder Patternとこれで同じことができています。コード量、可読性、段違いですね。

呼び出しも、オプショナルのところは名前付きで渡してあげれば自由に書けます。

Java likeに書いてみる

上のように簡単に書けるのはわかりましたが、Javaっぽく書くとどうなるのかも簡単に見ておきます。

`static class` だった `Builder` がScalaの場合はコンパニオンオブジェクトに移動するくらいですね。Javaと違って面倒なだけで、特に恩恵がないです。

Generalized type constrains

なんならここからが本題。Generalized type constrains を利用してさらにレベルアップさせます。

Scala 系のブログを漁ってたら少し古い下記の記事を見つけて知りました。コップ本にも、多分載って無い内容ですね。なので使い道とか調べるの難しい。。

yuroyoro.hatenablog.com

上記の記事に記載がありますが

Scala2.8から、Predefに<:<とか=:=とかが定義されていて、これなんだろ?とずーっと疑問だった訳ですよ。で、ついったーで質問投げてたらやっと理解できました。

"generalized type constraints"というヤツで、型パラメータに与えられた型が、特定の条件を満たす場合にのみ呼び出せるメソッドを定義できるというものです。しかもコンパイル時に静的にチェックされる!! これはスゴい!!

https://yuroyoro.hatenablog.com/entry/20100914/1284471301

type-safe builder

すこし話は戻って、オブジェクトの生成時に、例えば特定の順序で初期化する必要があったりすることがあります(あるそうです。自分はそういうシーンにはまだ遭遇したことがない)。これまでの Builder Pattern の実装では、実行時にしかそのことを検証できませんでしたが、type-safe builder と呼ばれる実装方法を使うと、コンパイル時に検証できるようになります。

gist.github.com

これで、例えば setFirstName を抜いて build しようとしてもIntelliJ なら静的解析の段階でエラーを出してくれますし、当然コンパイルエラーにもなります。順序を保証したい時とかに便利ですね。

最後に

Java like なBuilder PatternをScalaで使うことは基本的にはそんななく、多くはstatic factoryや、case class のデフォルトパラメータで十分だろうなと思います。type-safe builder に関してなかなかこれが必要な制約下になることはあんまり想像つきませんが、有用かなと思います。

Haskell 勉強メモ9

セクション

中置関数に対して、セクションという機能を使って部分適用をすることができる。

divideByTen :: (Floating a) => a -> a
divideByTen = (/10)

*Main> divideByTen 30
3.0

これは 30 / 10 と同義。

高階関数

関数を受け取り、2回適用する関数を書いてみる。先にScalaで書いてみよう。

def applyTwice[T](f: T => T)(x: T): T = f(f(x))

applyTwice { v: Int => v * 2 }(10) // 40

これを Haskell で書くとこうなる。

applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)

*Main> applyTwice (\a -> a * 2) 10
40

`(\a -> a * 2)` は Haskell無名関数

zipWith

標準ライブラリの zipWith

まずはHaskell標準の zipWith の挙動を確認する。

*Main> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]

zipWith は 2つの引数をとって1つの値を返す関数と、2つのリストを引数に取り、2つのリストの各要素に関数を適用することで、1つに結合する。

*Main> zipWith (\a b -> a + b) [1,2,3] [4,5,6]
[5,7,9]

Scalaには zipWith に相当するものはないが、zip メソッドが collection 系には実装されている

List(1, 2, 3) zip List(2, 3, 4) // res2: List[(Int, Int)] = List((1,2), (2,3), (3,4))

2つのリストを引数にとって、前から順に同じ位置にある要素で構成されたタプルのリストを作って返す。これに map を適用してあげれば、先程の haskellzipWith 相当のことが可能だ。

List(1, 2, 3) zip List(2, 3, 4) map { case (a, b) => a + b } // res2: List[Int] = List(3, 5, 7)

zipWith を実装してみる。※ 途中で、gist を使えば haskell もsyntax highlight いけることに気づいたので少し行数があるやつは試しに。

Scalaで同じようなことを書くとこんな感じ。最初の zipmap を連携させる方が読みやすいし書きやすい。

flip

普段Scalaを使っていて、さっきの zipWith なんかは似たようなもの、似たようなことを Scala でもやったことがあるが、これはhaskell本で初めてみる。

flip は関数を引数にとり、引数が入れ替わった関数を返す。※正直、何が嬉しいのか全く分からない。

Main> :t flip
flip :: (a -> b -> c) -> b -> a -> c
*Main> zip [1,2,3,4,5] "hello"
[(1,'h'),(2,'e'),(3,'l'),(4,'l'),(5,'o')]
*Main> flip zip [1,2,3,4,5] "hello"
[('h',1),('e',2),('l',3),('l',4),('o',5)]

zip は1つ目の配列と2つ目の配列の同じ位置にある要素で作ったタプルのリストを返す関数だ。1つ目の通常の zip では左の配列にあったものがタプルの左に、右の配列にあったものがタプルの右側にあるのが分かる。

flip を適用した zip はそれが逆になっている。:t で見てみるとよく分かるが、引数が入れ替わっていることが分かる。

*Main> :t zip
zip :: [a] -> [b] -> [(a, b)]
*Main> :t flip zip
flip zip :: [b] -> [a] -> [(a, b)]

自分で実装すると下記のようになる。