LoginSignup
4
1

More than 5 years have passed since last update.

Scalaでcase classを展開して引数として渡す方法

Last updated at Posted at 2018-11-22

TL;DR

やりたいこと

関数に対してcase classをそのまま渡して実行したい。

scala > def printProfile(name: String, age: Int): Unit = println(s"${age}歳の${name}です")
scala > case class Profile(name: String, age: Int)
scala > val p = Profile("山田", 25)

scala > printProfile(p.name, p.age) // いちいち引数を書くのが面倒くさい!
25歳の山田です
scala > printProfile.よしなにしてくれるメソッド(p) // case classインスタンスpを渡すだけで実行してほしい!
25歳の山田です

できるようにしました → bigwheel/case-class-expand-and-apply

ライブラリを実装して次のように実行できるようにしました1

scala > (printProfile _).expandAndApply(p)
25歳の山田です

ライブラリの利用方法はいつもの感じです。READMEを参照してください。

Scala 2.13で追加されるpipeメソッドに合わせたもの(expandAndApplyのレシーバと引数を入れ替えたもの)も使えます。

scala > p.expandAndPipe(printProfile _)
25歳の山田です

背景詳細

scala > def printProfile(name: String, age: Int): Unit = println(s"${age}歳の${name}です")

こういうメソッドがあるとします。

普通に呼び出すときは

scala> printProfile("山田", 25)
25歳の山田です

これで良いですが、この引数を1セットで引き回したいときがあります。

そんなときはTupleを使う手があります。

scala> val profile = ("山田", 25)
profile: (String, Int) = (山田,25)

scala> printProfile(profile._1, profile._2)
25歳の山田です

手で_1, _2と展開するのが面倒になったときは以下の書き方もあります(参考元)2

scala> (printProfile _).tupled(profile)
25歳の山田です

ただ、Tupleは中身の型とその順番でのみ一意性が定義されます。
言い換えると、中身の値の型とその順番が一緒だとまったく意図していないインスタンスでも以下のように実行できてしまいます。

scala> val item = ("ファンタ", 120)
scala> (printProfile _).tupled(item)
120歳のファンタです

そこでcase classが導入されるのは自然な流れです3

scala> case class Profile(name: String, age: Int)
scala> val ccProfile = Profile("山田", 25)

ただしcase classはTupleではないのでそのままだとまた手書きで展開する必要があります。

scala> printProfile(ccProfile.name, ccProfile.age)
25歳の山田です

Tupleで一度解決した問題がまた発生しました。

case classはCompanion Objectにunapplyというcase classインスタンスを対応するTupleへ変換するメソッドが必ず定義されます。それを利用すれば以下のようにできます。

scala> (printProfile _).tupled(Profile.unapply(ccProfile).get)
25歳の山田です

ですが Profile.unapply(ccProfile).get のところにぎこちなさを感じます。
確実に変換できることが保証されているにもかかわらず unapply の仕様上 Option 型でくるんで返されるためgetメソッドを必ず書いてやる必要があります。またコンパニオンオブジェクトである Profile のメソッドを呼んでいることも微妙な気がします。ccProfile.toTuple みたいなインスタンスメソッドがほしいですね。

あります。shapelessライブラリを使いましょう。

scala> import shapeless.syntax.std.product._
scala> (printProfile _).tupled(ccProfile.toTuple)
25歳の山田です

これでかなりマシになりました。
ただ、論理的には「関数へcase classインスタンスを渡す」というように表現したいにもかかわらずコード中でTupleを中継していることが気になります。
implicit classを使いFunction2にメソッドを追加してTupleを隠蔽しましょう。

scala> :paste
// Entering paste mode (ctrl-D to finish)

import shapeless.ops.product.ToTuple
implicit class RichFunction2[T1, T2, R](f: (T1, T2) => R) {
  type ARGUMENTS = (T1, T2)
  def expandAndApply[CASECLAZZ <: Product](t: CASECLAZZ)
    (implicit toTuple: ToTuple.Aux[CASECLAZZ, ARGUMENTS]): R = {
    f.tupled(t.toTuple[ARGUMENTS])
  }
}

// Exiting paste mode, now interpreting.

scala> (printProfile _).expandAndApply(ccProfile)
25歳の山田です

やりました! scalaの言語仕様ギリギリまでシンプルに表現できたと思います。

ただしこのRichFunctionNは1から22のすべてのFunctionNトレイトに対して定義してやる必要があるのでかなり手間です。
そこでライブラリ化した、というのが今回の背景になります。

残タスク

今回やりたかったことはほぼほぼ達成したのですが、今回のコードは結局関数とcase classを引き合わせるために合流点でTupleを使っている関係上実はcase classの型安全性は損なわれています。
どういうことかというとつまり変数の型とその順番さえ一致していればどのようなcase classでもexpandAndApplyを呼び出せるようになっているということです。Tupleの項目で言及してものと同じ問題が発生しているんですね。

間違ったcase classを渡す危険性を少しでも下げるために、例えば各引数とcase classのメンバーの変数名が一致するかをマクロでチェックしてもいいかもしれません。
偶然変数の型と順番が一致することはあっても、変数名が一致することはそうそうないだろうという前提に基づいた工夫ですね。

ただまああくまで小手先に過ぎない上、変数名を厳密に一致させる必要があるというデメリットも生まれてしまうため微妙な気もします。やるとしても expandAndApply とは別の名前にしたほうが良さそうですね( expandAndApplyStrictly など)。

作成動機

そもそもなぜこんなライブラリを作ったかというと、Pythonのようにcase classの各要素を引数リストへunpackしたいと思ったことがきっかけです。
実用性や筋の良し悪しはともかく、作成中はマクロを勉強したりScala本体のFunctionX, Productなどのコードを読んだりで非常に楽しかったので良い経験になりました。

注意書き(あるいは免責)

このやりたいことに対して手段が適当でない気もずっとしていますが、こういった方法もあるよということで作ってみました。
Scalaのそこそこの歴史でもこういったことが具体的にしたい、または実現したというような話がないのでみんな必要性を感じていないか、そもそも解決法が間違っているのではないかと思います。
そもそも筋が悪い、こっちがScala way的なツッコミあればコメントぜひお願いします(丸投げ)。

参考元


  1. expandAndApplyというメソッド名がダサいとかメソッドを記号化したほうが( <<| とかね)使いやすくない?とかあるんですがいい名前が浮かばなかったのとscalaオフィシャルでもメソッドの記号表記は今逆風っぽいので記号化は一旦していません。真っ当な理由があったらメソッドのリネーム、エイリアス定義は前向きに検討しますのでプルリクください 

  2. ちなみにprintProfileをFunction2型として扱うためのxxx _はScalaの言語仕様上避けられないようです。正直ここはちょっとイケてないなと思います。トレードオフになりますが、例えば関数実行には必ず()をつける、という文法にしてしまえばxxx _は要らなくなるでしょう 

  3. 実際のところ、scalaではcase classが簡単に定義できること、上記のTupleの型的な危険性から動的型付け言語ではTupleが使われるシーンのほとんどでcase classが使われます。Tupleが使われるときはcase classの定義すら省略したいほど局所的に値の組み合わせを一緒に引き回したいときで、具体的には無名関数を定義するとき、複数の値をメソッドの返り値として返したいときなどがあります 

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1