トイレクラスで見る凝集度と責務
社内向けに凝集度と責務について発表しました。
正直、プレゼン形式での発表は慣れないので全然うまく話せませんでしたが、調べるためのインデックスができればいいくらいの気持ちと、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
このうち、致命的な例外と非検査例外については捕捉せず、検査例外に対してのみ呼び出し側でハンドリングをします。
エラーの種類 ハンドリング 例 致命的なエラー( Error
)No OutOfMemoryError
, StackOverflowError
など。非検査例外( RuntimeException
)No IllegalArgumentException
など。バグ起因系。検査例外( Exception
)Yes 業務例外など。
基本的に、検査例外以外はハンドリングする必要がありません。※ハンドリングすべきでない。
Scalaにおける例外
Scalaにおいては、検査例外をなくし、例外は全て致命的なエラーもしくは、非検査例外のハンドリングしてはならないもの、しなくて良いものだけになりました。
- 致命的な例外
- 非検査例外
のみ。
検査例外をどう扱うか?
Scalaで検査例外を扱う際は型で表現します。Try
や Either
などですね。Option
もそうです。この辺りの詳しい説明はドワンゴの新卒研修資料がわかりやすいと思います。
これにより、型で利用者側が明確に例外処理をいけないんだなと分かりますし、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 }
こんな感じにしてあげると、PurchaseError
は sealed 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 コードが複雑になる
これは単純です。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
をしないようにしよう。安易なデフォルト値はバグの見落としに繋がる。 - 仕様上、必ず存在する値だとわかったら、迷わず
get
でOption
から取り出そう。大丈夫、もしエラーが出た時は、バグを発見できたと喜ぼう。
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にする理由はないですね。単純に開発用途以外で使いづらいです。世の中、WindowsとMac向けにクライアントソフトは開発されているので。実際、一時期黒い画面でなんかすごいことしてそうな人のイメージに憧れて、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
ScalaでJava 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 系のブログを漁ってたら少し古い下記の記事を見つけて知りました。コップ本にも、多分載って無い内容ですね。なので使い道とか調べるの難しい。。
上記の記事に記載がありますが
Scala2.8から、Predefに<:<とか=:=とかが定義されていて、これなんだろ?とずーっと疑問だった訳ですよ。で、ついったーで質問投げてたらやっと理解できました。
"generalized type constraints"というヤツで、型パラメータに与えられた型が、特定の条件を満たす場合にのみ呼び出せるメソッドを定義できるというものです。しかもコンパイル時に静的にチェックされる!! これはスゴい!!
https://yuroyoro.hatenablog.com/entry/20100914/1284471301
type-safe builder
すこし話は戻って、オブジェクトの生成時に、例えば特定の順序で初期化する必要があったりすることがあります(あるそうです。自分はそういうシーンにはまだ遭遇したことがない)。これまでの Builder Pattern の実装では、実行時にしかそのことを検証できませんでしたが、type-safe builder と呼ばれる実装方法を使うと、コンパイル時に検証できるようになります。
これで、例えば 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
を適用してあげれば、先程の haskell の zipWith
相当のことが可能だ。
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で同じようなことを書くとこんな感じ。最初の zip
と map
を連携させる方が読みやすいし書きやすい。
flip
普段Scalaを使っていて、さっきの zi
pWith なんかは似たようなもの、似たようなことを 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)]
自分で実装すると下記のようになる。