レンズ: レンズとは何か、レンズを使用する理由 ;)

14862 ワード

これは私の最初の投稿です.楽しんでいただければ幸いです.
どのトピックについて話すことができるかを考えていたところ、ここ数週間で出会ったトピックを思いつきました.

私は N26 バルセロナ オフィスのバックエンド ソフトウェア エンジニアです. N26 では kotlin を使用し、ご想像のとおり、デジタル バンクを実行するマイクロサービスを構築します.

私たちはバックオフィス マイクロサービスに取り組んでいました.これは、何らかの方法で顧客に関するデータを処理し、それらを変更するためにいくつかのエンドポイントを公開しました (CRUD のようですが、正確ではありません).

したがって、同じデータを使用して問題を特定することはできません.これらは特権があるためです.しかし、より簡単な例を使用して同じ問題を説明することはできます.

Person に関する情報を格納するためのモデルがアプリケーションにあるとします.

data class Person(
 val name: String, 
 val address: Address
)

data class Address(
 val streetName: String, 
 val number: String, 
 val city: String
) 


エンドポイントがあり、個人に関する情報を更新するドメイン サービスがあるとします.次のようなインターフェイスがあるとします.

interface UpdateAddressService {

 fun updateStreetName(
   person: Person, 
   newStreetName: String
 ): Person
}


kotlin 言語だけを使用してそのようなインターフェースを実装しようとすると、次のようなクラスになる可能性があります.

class SimpleUpdateAddressService: UpdateAddressService {

 fun updateStreetName(
   person: Person, 
   newStreetName: String
 ): Person {
  val newAddress = 
    person.address.copy(streetName = newStreetName)
  val updatedPerson = 
    person.copy(address = newAddress)
  return updatedPerson
 }
}


これで、物事が簡単に面倒になることが想像できます.たとえば、streetName が次のような別の値オブジェクトである場合を想像してください.

data class StreetName(
 val streetType: String,
 val name: String
) 


次に、次のような結果になります.

class SimpleUpdateAddressService: UpdateAddressService {

 fun updateStreetName(
   person: Person, 
   newStreetName: String
 ): Person {
  val updatedStreetName = 
    person.address.streetName.copy(name = newStreetName)
  val newAddress = 
    person.address.copy(streetName = updatedStreetName)
  val updatedPerson = 
    person.copy(address = newAddress)
  return updatedPerson
 }
}


ご覧のとおり、モデルがネストされ始めるとすぐに、必要な単純な更新を行うのに時間がかかります.

関数型プログラミングには、レンズと呼ばれる単純な概念があります.レンズは、ソースから何かを取得できる非常にシンプルなインターフェイスです.これをターゲットと呼び、ソース値を指定して新しいターゲットを設定することもできます.

interface Lens<S, T> {

  fun get(s: S): T
  fun set(s: S, newT: T): S
}


関数型プログラミングでよくある本当の力は、この構造がセミグループと呼ばれるものをサポートしていることです.つまり、2 つのレンズを取り、それらを組み合わせる操作を定義でき、この操作は連想的です.

たとえば、この combine メソッドを Lens インターフェイスで次のように定義できます.

interface Lens<S, T> {

  fun get(s: S): T
  fun set(newT: T, s: S): S

  fun <A> combine(l2: Lens<T, A>): Lens<S, A> {
    val self = this
    return object : Lens<S, A> {

      override fun get(s: S): A {
        val function: (s: S) -> A = self::get andThen l2::get
        return function(s)
      }

      override fun set(newT: A, s: S): S {
        val newT1 = l2.set(newT, self.get(s))
        return self.set(newT1, s)
      }
    }
  }
}


これは基本的に arrow-kt が提供するものであり、Lens インターフェイスと必要なすべてのレンズを自分で定義する必要はありません.

Arrow 1.0 は kapt kotlin annotation processor を使用し、@optics アノテーションを使用すると、コンパイル時に生成されたコードを次のように活用できます.

@optics
data class Person(
 val name:String, 
 val address: Address
) {
 companion object
}

@optics
data class Address(
 val streetName: String, 
 val number: String, 
 val city: String
) {
 companion object
}


したがって、これらの新しく定義されたデータ クラスを使用して、以前のインターフェイスを別の方法で実装できます.

class ArrowUpdateAddressService: UpdateAddressService {
 private val lens = Person.address.streetname
 fun updateStreetName(
   person: Person, 
   newStreetName: String
 ): Person {
    return lens.set(source = person, focus = newStreetName)
 }
}


値オブジェクトが StreetName の場合は、レンズを変更するだけです.

class ArrowUpdateAddressService: UpdateAddressService {
 private val lens = Person.address.streetname.name
 // unchanged code


この構造がいかに強力でクリーンであるかがわかります.

この記事を楽しんでいただければ幸いです.

またね!

参考文献



表紙の画像は Wikimedia からの引用です