Scalaの暗黙的理解

画像



最近、Javaの世界の友人とScalaを使った経験について何度か会話をしました。 ほとんどはScalaを改良されたJavaとして使用し、最終的には失望しました。 主な批判は、Scalaは非常に強力で自由度の高い言語であり、同じことがさまざまな方法で実装できるという事実に向けられていました。 さて、そして不満のケーキの桜は、もちろん、暗黙的です。 特に初心者にとって、暗黙は言語の最も物議を醸す機能の1つであることに同意します。 それが暗示するように、まさに「暗黙」という名前。 経験の浅い人では、暗黙の人が不適切なアプリケーション設計と多くのエラーを引き起こす可能性があります。 Scalaで作業している人は誰でも、依存関係の解決中に少なくとも一度エラーに遭遇したことがあると思います。 どこを見ますか? 問題を解決する方法は? その結果、私はグーグルで検索する必要がありました。もちろん、もしあればライブラリのドキュメントを読む必要がありました。 通常、必要な依存関係をインポートすることで解決策が見つかり、次回まで問題は忘れられます。



この投稿では、暗黙の使用の一般的な慣習について話し、それらをより「明示的」かつ理解しやすくするのに役立ちたいと思います。 最も一般的な使用例:





ネットワークには、このトピックに関する多くの記事、ドキュメント、レポートがあります。 しかし、すばらしいJavaライブラリーのためにScalaに優しいAPIを作成する例について、彼らの実用的なアプリケーションについて詳しく述べたいと思います。 タイプセーフ Lightbend Config 。 最初に質問に答える必要がありますが、実際、ネイティブAPIの何が問題になっていますか? ドキュメンテーションから例を見てみましょう。



import com.typesafe.config.ConfigFactory val conf = ConfigFactory.load(); val foo = config.getString("simple-lib.foo") val bar = config.getInt("simple-lib.bar")
      
      





ここには少なくとも2つの問題があります。



  1. エラー処理。 たとえば、 getInt



    メソッドgetInt



    目的の型の値を返せない場合、例外がスローされます。 そして、例外なく「クリーンな」コードを書きたいと思います。
  2. 拡張性。 このAPIはいくつかのJava型をサポートしていますが、型サポートを拡張したい場合はどうでしょうか?


2番目の問題から始めましょう。 標準のJavaソリューションは継承です。 新しいメソッドを追加することで、基本クラスの機能を拡張できます。 コードを所有している場合、これは通常問題になりませんが、サードパーティのライブラリの場合はどうでしょうか? Scalaでの「単純な」ソリューションパスは、暗黙的なクラスまたは「Pimp My Library」パターンを使用することです。



 implicit class RichConfig(val config: Config) extends AnyVal { def getLocalDate(path: String): LocalDate = LocalDate.parse(config.getString(path), DateTimeFormatter.ISO_DATE) }
      
      





これで、ソースクラスで定義されているかのようにgetLocalDate



メソッドを使用できます。 悪くない。 ただし、ローカルでのみ問題を解決し、1つのRichConfig



クラスですべての新機能をサポートするか、異なるメソッドで同じメソッドが定義されている場合、「Ambiguous implicit values」エラーが発生する可能性があります。



これを改善する方法はありますか? ここで、通常Javaでは、継承がポリモーフィズムの実装に使用されることを思い出してください。 実際、多型にはさまざまなタイプがあります。



  1. アドホックポリモーフィズム。
  2. パラメトリック多型。
  3. サブタイプ多型。


継承は、サブタイプ多型を実装するために使用されます。 アドホックポリモーフィズムに興味があります。 これは、パラメーターのタイプに応じて異なる実装を使用することを意味します。 Javaでは、これはメソッドのオーバーロードを使用して実装されます。 Scalaでは、型クラスを使用してさらに実装できます。 この概念は、言語に組み込まれているHaskelから生まれたものであり、Scalaでは、暗黙の実装が必要なパターンです。 つまり、タイプクラスはコントラクトです。たとえば、タイプT



によってパラメーター化されたトレイトFoo[T]



、暗黙的な依存関係を解決するために使用され、コントラクトの目的の実装はタイプによって選択されます。 紛らわしいように聞こえますが、本当に簡単です。



例を見てみましょう。 このケースでは、構成から値を読み取るためのコントラクトを定義します。



 trait Reader[A] { def read(config: Config, path: String): Either[Throwable, A] }
      
      





ご覧のとおり、 Reader



特性はタイプA



パラメーター化されていますA



最初の問題を解決するために、 Either



を返します。 これ以上の例外はありません。 コードを簡素化するために、型エイリアスを作成できます。



 trait Reader[A] { def read(config: Config, path: String): Reader.Result[A] } object Reader { type Result[A] = Either[Throwable, A] def apply[A](read: (Config, String) => A): Reader[A] = new Reader[A] { def read[A](config: Config, path: String): Result[A] = Try(read(config, path)).toEither } implicit val intReader = Reader[Int]((config: Config, path: String) => config.getInt(path)) implicit val stringReader = Reader[String]((config: Config, path: String) => config.getString(path)) implicit val localDateReader = Reader[LocalDate]((config: Config, path: String) => LocalDate.parse(config.getString(path), DateTimeFormatter.ISO_DATE);) }
      
      





taipクラスReaderを定義し、タイプInt



String



LocalDate



実装をいくつか追加しました。 次に、 Config



型クラスの操作方法を教える必要があります。 そして、ここで「Pimp My Library」パターンと暗黙の引数はすでに便利です。



 implicit class ConfigSyntax(config: Config) extends AnyVal { def as[A](path: String)(implicit reader: Reader[A]): Reader.Result[A] = reader.read(config, path) }
      
      





コンテキスト境界を使用して、より簡単に書き換えることができます。



 implicit class ConfigSyntax(config: Config) extends AnyVal { def as[A : Reader](path: String): Reader.Result[A] = implicitly[Reader[A]].read(config, path) }
      
      





そして今、使用例:



 val foo = config.as[String]("simple-lib.foo") val bar = config.as[Int]("simple-lib.bar")
      
      





型クラスは、簡単に拡張可能なコードを書くことができる非常に強力なメカニズムです。 新しい型のサポートが必要な場合は、目的の型のクラスの実装を記述してコンテキストに配置するだけです。 また、暗黙的な依存関係の解決に優先順位を使用すると、標準の実装をオーバーライドできます。 たとえば、 LocalDate



リーダーの別のバージョンを定義できます。



 implicit val localDateReader2 = Reader[LocalDate]((config: Config, path: String) => Instant .ofEpochMilli(config.getLong(path)) .atZone(ZoneId.systemDefault()) .toLocalDate() )
      
      





ご覧のとおり、暗黙的に正しく使用すると、クリーンで拡張可能なコードを記述できます。 ソースコードを変更せずに、サードパーティライブラリの機能を拡張できます。 一般化されたコードを記述し、型クラスを使用してアドホックポリモーフィズムを使用できます。 複雑なクラス階層について心配する必要はありません;機能を部分に分割し、それらを個別に実装できます。 分割の原則と実行中のルール。



サンプル付きのGithubプロジェクト。



All Articles