如何使用Arrow编写多态程序



哈Ha!

我叫Artyom Dobrovinsky,我在Finch工作。 我建议阅读Arrow函数编程库之父之一的文章,内容涉及如何编写多态程序。 通常,刚开始以功能风格开始写作的人并不急于摒弃旧习惯,实际上,他们使用DI容器和继承编写了更优雅的命令。 无论使用哪种类型,重用函数的想法都可能促使许多人朝正确的方向思考。

好好享受


***


如果我们可以编写应用程序而不考虑运行时将使用的数据类型,而仅仅描述如何处理这些数据怎么办?


想象一下,我们有一个与RxJava库中的Observable类型兼容的应用程序。 这种类型使我们可以编写带有数据的调用和操作链,但是最后,这个Observable是否将不仅仅是具有附加属性的容器?


具有相同类型的故事,如Flowable, Deferred (协程), FutureIO以及许多其他类型。


从概念上讲,所有这些类型都代表一个操作(已经完成或计划在将来实现),该操作支持诸如将内部值转换为另一种类型( map )的操作,使用flatMap来创建相似类型的操作链,并结合相同类型的其他实例( zip )等


为了基于这些行为编写程序,同时保持声明性描述,并使程序独立于诸如Observable类的特定数据类型Observable使用的数据类型与某些协定(例如mapflatMap等)相对应Observable足够了。


这种方法可能看起来很奇怪或太复杂,但它具有有趣的优点。 首先,考虑一个简单的示例,然后再讨论它们。


规范问题


假设我们有一个带有待办事项列表的应用程序,并且我们想从本地缓存中提取Task类型的对象列表。 如果在本地存储中找不到它们,我们将尝试通过网络查询它们。 我们需要为这两个数据源提供一个合约,以便它们都可以为合适的User对象获得Task类型的对象列表,而与数据源无关:


 interface DataSource { fun allTasksByUser(user: User): Observable<List<Task>> } 

在这里,为简单起见,我们返回Observable ,但它可以是SingleMaybeFlowableDeferred -任何适合实现目标的东西。


添加一些数据源的Mocha实现,一个用于 ,另一个用于


 class LocalDataSource : DataSource { private val localCache: Map<User, List<Task>> = mapOf(User(UserId("user1")) to listOf(Task("LocalTask assigned to user1"))) override fun allTasksByUser(user: User): Observable<List<Task>> = Observable.create { emitter -> val cachedUser = localCache[user] if (cachedUser != null) { emitter.onNext(cachedUser) } else { emitter.onError(UserNotInLocalStorage(user)) } } } class RemoteDataSource : DataSource { private val internetStorage: Map<User, List<Task>> = mapOf(User(UserId("user2")) to listOf(Task("Remote Task assigned to user2"))) override fun allTasksByUser(user: User): Observable<List<Task>> = Observable.create { emitter -> val networkUser = internetStorage[user] if (networkUser != null) { emitter.onNext(networkUser) } else { emitter.onError(UserNotInRemoteStorage(user)) } } } 

两个数据源的实现几乎相同。 这些只是这些源的模拟版本,理想情况下是从本地存储或网络API中提取数据。 在这两种情况下,都使用Map<User, List<Task>>来存储数据。


因为 我们有两个数据源,我们需要以某种方式进行协调。 创建一个存储库:


 class TaskRepository(private val localDS: DataSource, private val remoteDS: RemoteDataSource) { fun allTasksByUser(user: User): Observable<List<Task>> = localDS.allTasksByUser(user) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.computation()) .onErrorResumeNext { _: Throwable -> remoteDS.allTasksByUser(user) } } 

它只是尝试从LocalDataSource加载List<Task> ,如果找不到,则尝试使用RemoteDataSource从网络请求它们。


我们创建一个简单的模块来提供依赖关系,而无需使用任何框架进行依赖关系注入(DI):


 class Module { private val localDataSource: LocalDataSource = LocalDataSource() private val remoteDataSource: RemoteDataSource = RemoteDataSource() val repository: TaskRepository = TaskRepository(localDataSource, remoteDataSource) } 

最后,我们需要一个简单的测试来运行整个操作堆栈:


 object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val dependenciesModule = Module() dependenciesModule.run { repository.allTasksByUser(user1).subscribe({ println(it) }, { println(it) }) repository.allTasksByUser(user2).subscribe({ println(it) }, { println(it) }) repository.allTasksByUser(user3).subscribe({ println(it) }, { println(it) }) } } } 

以上所有代码都可以在github上找到


该程序为三个用户组成执行链,然后订阅生成的Observable


User类型的前两个对象可用,有了这个我们很幸运。 User1在本地DataSource中可用,而User2在远程中可用。


但是User3存在问题,因为它在本地存储中不可用。 该程序将尝试从远程服务下载它-但是也不存在。 搜索将失败,并且我们将在控制台中显示错误消息。


这是这三种情况在控制台中显示的内容:


 > [Task(value=LocalTask assigned to user1)] > [Task(value=Remote Task assigned to user2)] > UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

我们完成了一个示例。 现在让我们尝试以 的方式对此逻辑进行编程。


数据类型抽象


现在, DataSource接口的合同将如下所示:


 interface DataSource<F> { fun allTasksByUser(user: User): Kind<F, List<Task>> } 

一切似乎都相似,但是有两个重要的区别:


  • 依赖于广义类型(泛型) F
  • 该函数返回的类型现在为Kind<F, List<Task>>

Kind是Arrow编码所谓的一种 (higher kind)
我将用一个简单的例子来解释这个概念。


Observable<A>有2个部分:


  • Observable :容器,固定类型。
  • A :泛型类型的参数。 其他类型可以传递给的抽象。

我们习惯于将像A这样A泛型类型作为抽象。 但是很少有人知道我们还可以抽象如Observable容器类型。 为此,有很多类型。


这个想法是我们可以有一个像F<A>这样的构造函数,其中FA都可以是一个泛型类型。 Kotlin编译器尚不支持此语法( 仍然吗? ),因此我们将使用类似的方法来模仿它。


Arrow通过使用中间元接口Kind<F, A>支持这一点,该中间元接口包含到两种类型的链接,并且还在编译期间在两个方向上生成转换器,以便您可以遵循Kind<Observable, List<Task>>的路径Kind<Observable, List<Task>>Observable<List<Task>> ,反之亦然。 不是理想的解决方案,而是可行的解决方案。


因此,再次查看存储库的接口:


 interface DataSource<F> { fun allTasksByUser(user: User): Kind<F, List<Task>> } 

DataSource函数返回一个高类型: Kind<F, List<Task>> 。 它转换为F<List<Task>> ,其中F仍然是广义的。


我们仅捕获签名中的List<Task> 。 换句话说,我们并不关心将使用哪种类型的F容器,只要它包含List<Task> 。 我们可以将不同的数据容器传递给函数。 已经清楚了吗? 来吧


让我们看一下以这种方式实现的DataSource ,但这一次是每个单独的。 首先到本地:


 class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A { private val localCache: Map<User, List<Task>> = mapOf(User(UserId("user1")) to listOf(Task("LocalTask assigned to user1"))) override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } ) } 

添加了许多新内容,我们将逐步分析所有内容。


DataSource保留通用类型F因为它实现了DataSource<F> 。 我们希望保持从外部传输这种类型的可能性。


现在,忘记构造函数中可能不熟悉的ApplicativeError ,而将注意力放在allTasksByUser()函数上。 然后我们将返回ApplicativeError


 override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } ) 

可以看出,它返回Kind<F, List<Task>> 。 我们仍然不在乎容器F什么,只要它包含List<Task>


但是有一个问题。 根据我们是否可以在本地存储中找到所需用户的Task对象列表,我们要报告一个错误(找不到Task )或返回已包装在F Task (找到Task )。


对于这两种情况,我们都需要返回: Kind<F, List<Task>>


换句话说:存在一个我们对( F )一无所知的类型,我们需要一种方法来返回包装在该类型中的错误。 另外,我们需要一种创建此类型实例的方法,其中将成功完成该函数后获得的值进行包装。 听起来好像不可能?


让我们回到类声明,注意将ApplicativeError传递给构造函数,然后用作类的委托( by A )。


 class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A { //... } 

ApplicativeErrorApplicative继承的,它们都是类型类。


类型类定义行为(合同)。 它们被编码为使用通用类型形式的参数的接口,例如Monad<F>Functor<F>等。 此F是数据类型。 这样,我们可以传递诸如EitherOptionIOObservableFlowable等类型。


因此,回到我们的两个问题:


  • 将函数成功完成后获得的值包装在Kind<F, List<Task>>

为此,我们可以使用Applicative类型的类。 由于ApplicativeError继承自它,因此我们可以委派其属性。


Applicative只是提供了just(a)函数。 just(a)将值包装在任何高类型的上下文中。 因此,如果我们有Applicative<F> ,则无论该值是什么,它都可以just(a)调用just(a)将值包装在容器F中。 假设我们使用一个Observable ,我们将有一个Applicative<Observable> ,它知道如何在Observable包装a以得到Observable.just(a)


  • 将错误包装在实例Kind<F, List<Task>>

为此,我们可以使用ApplicativeError 。 它提供了一个函式raiseError(e) ,将错误包装在类型为F的容器中F 对于Observable示例,该错误将创建类似Observable.error<A>(t)的错误,其中tThrowable ,因为我们将错误类型声明为ApplicativeError<F, Throwable>类型的类。


看看我们的LocalDataSource<F>抽象实现。


 class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A { private val localCache: Map<User, List<Task>> = mapOf(User(UserId("user1")) to listOf(Task("LocalTask assigned to user1"))) override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } ) } 

内存中存储的Map<User, List<Task>>保持不变,但是现在该函数做了几件您可能不熟悉的事情:


  • 她尝试从本地缓存加载Task列表,并且由于返回值可能为null (可能找不到Task ),因此我们使用Option对此模型进行建模。 如果不清楚Option是如何工作的,则它将对包装在其中的值的存在与否进行建模。


  • 收到可选值后,我们在其顶部调用fold 。 这等效于在可选值上使用when 。 如果缺少该值,则Option会将错误包装在数据类型F (传递的第一个lambda)。 并且,如果存在该值,则Option为数据类型F (第二个lambda)创建一个包装实例。 在这两种情况下,都使用前面提到的ApplicativeError属性: raiseError()just()



因此,我们使用类对数据源的实现进行了抽象,以使它们不知道哪种容器将用于F类型。


实施网络DataSource如下所示:


 class RemoteDataSource<F>(A: Async<F>) : DataSource<F>, Async<F> by A { private val internetStorage: Map<User, List<Task>> = mapOf(User(UserId("user2")) to listOf(Task("Remote Task assigned to user2"))) override fun allTasksByUser(user: User): Kind<F, List<Task>> = async { callback: (Either<Throwable, List<Task>>) -> Unit -> Option.fromNullable(internetStorage[user]).fold( { callback(UserNotInRemoteStorage(user).left()) }, { callback(it.right()) } ) } } 

但是有一个小的区别:我们没有使用委派给ApplicativeError实例的方法,而是使用了另一个类: Async


这是因为网络调用本质上是异步的。 我们想编写将异步执行的代码,使用为此设计的类型类是合乎逻辑的。


Async用于模拟异步操作。 它可以模拟任何回调操作。 请注意,我们仍然不知道特定的数据类型;我们仅描述本质上异步的操作。


考虑以下功能:


 override fun allTasksByUser(user: User): Kind<F, List<Task>> = async { callback: (Either<Throwable, List<Task>>) -> Unit -> Option.fromNullable(internetStorage[user]).fold( { callback(UserNotInRemoteStorage(user).left()) }, { callback(it.right()) } ) } 

我们可以使用async {}函数,该函数由Async类型的类提供给我们,用于对操作进行建模,并创建将Async创建的Kind<F, List<Task>>类型的实例。


如果我们使用类似Observable的固定数据类型,则Async.async {}等同于Observable.create() ,即 创建可以从同步或异步代码(例如ThreadAsyncTask调用的操作。


callback参数用于将所得的回调链接到容器上下文F ,这是一个高类型。


因此,我们的RemoteDataSource抽象的,并且依赖于类型F仍然未知的容器F


让我们进入抽象级别,再看看我们的存储库。 如果您还记得的话,首先我们需要在LocalDataSource搜索Task对象,然后再(如果在本地未找到它们)从RemoteLocalDataSource请求它们。


 class TaskRepository<F>( private val localDS: DataSource<F>, private val remoteDS: RemoteDataSource<F>, AE: ApplicativeError<F, Throwable>) : ApplicativeError<F, Throwable> by AE { fun allTasksByUser(user: User): Kind<F, List<Task>> = localDS.allTasksByUser(user).handleErrorWith { when (it) { is UserNotInLocalStorage -> remoteDS.allTasksByUser(user) else -> raiseError(UnknownError(it)) } } } 

ApplicativeError<F, Throwable>又来了! 它还提供了handleErrorWith()函数,该函数可在任何高端接收器之上使用。


看起来像这样:


 fun <A> Kind<F, A>.handleErrorWith(f: (E) -> Kind<F, A>): Kind<F, A> 

因为 localDS.allTasksByUser(user)返回Kind<F, List<Task>> ,可以将其视为F<List<Task>> ,其中F仍然是通用类型,我们可以在其顶部调用handleErrorWith()


handleErrorWith()允许您使用传递的lambda响应错误。 让我们仔细看一下该函数:


 fun allTasksByUser(user: User): Kind<F, List<Task>> = localDS.allTasksByUser(user).handleErrorWith { when (it) { is UserNotInLocalStorage -> remoteDS.allTasksByUser(user) else -> raiseError(UnknownError(it)) } } 

这样,我们得到第一个操作的结果,除非抛出异常。 异常将由lambda处理。 如果错误属于UserNotInLocalStorage类型,我们将尝试在远程DataSource查找Tasks类型的对象。 在所有其他情况下,我们将未知错误包装在F型容器中F


依赖模块仍然与以前的版本非常相似:


 class Module<F>(A: Async<F>) { private val localDataSource: LocalDataSource<F> = LocalDataSource(A) private val remoteDataSource: RemoteDataSource<F> = RemoteDataSource(A) val repository: TaskRepository<F> = TaskRepository(localDataSource, remoteDataSource, A) } 

唯一的区别是,它现在是抽象的,并且依赖于F ,后者仍然是多态的。 为了降低噪声水平,我故意不对此进行注意,但是Async继承自ApplicativeError ,因此可以在程序执行的所有级别将其用作其实例。


测试多态


最后,我们的应用程序完全从对容器( F )的特定数据类型的使用中抽象而来,我们可以集中精力在运行时测试多态性。 我们将测试同一段代码,将不同类型的数据传递给它,以获取类型F 该方案与我们使用Observable的情况相同。


该程序的编写方式使我们完全摆脱了抽象的边界,并可以根据需要传达实现细节。


首先,让我们尝试使用RxJava中的F Single作为容器。


 object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val singleModule = Module(SingleK.async()) singleModule.run { repository.allTasksByUser(user1).fix().single.subscribe(::println, ::println) repository.allTasksByUser(user2).fix().single.subscribe(::println, ::println) repository.allTasksByUser(user3).fix().single.subscribe(::println, ::println) } } } 

为了兼容,Arrow为著名的库数据类型提供了包装。 例如,有一个方便的SingleK包装器。 这些包装器允许您将类型类与数据类型结合使用,作为高类型。


控制台上将显示以下内容:


 [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

如果使用Observable将得到相同的结果。


现在,让我们使用Maybe可以使用MaybeK包装器:


 @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val maybeModule = Module(MaybeK.async()) maybeModule.run { repository.allTasksByUser(user1).fix().maybe.subscribe(::println, ::println) repository.allTasksByUser(user2).fix().maybe.subscribe(::println, ::println) repository.allTasksByUser(user3).fix().maybe.subscribe(::println, ::println) } } 

相同的结果将显示在控制台上,但现在使用不同的数据类型:


 [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

那么ObservableK / FlowableK呢?
让我们尝试一下:


 object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val observableModule = Module(ObservableK.async()) observableModule.run { repository.allTasksByUser(user1).fix().observable.subscribe(::println, ::println) repository.allTasksByUser(user2).fix().observable.subscribe(::println, ::println) repository.allTasksByUser(user3).fix().observable.subscribe(::println, ::println) } val flowableModule = Module(FlowableK.async()) flowableModule.run { repository.allTasksByUser(user1).fix().flowable.subscribe(::println) repository.allTasksByUser(user2).fix().flowable.subscribe(::println) repository.allTasksByUser(user3).fix().flowable.subscribe(::println, ::println) } } } 

我们将在控制台中看到:


 [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

一切正常。


让我们尝试使用DeferredK ,它是kotlinx.coroutines.Deferred类型的包装器:


 object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val deferredModule = Module(DeferredK.async()) deferredModule.run { runBlocking { try { println(repository.allTasksByUser(user1).fix().deferred.await()) println(repository.allTasksByUser(user2).fix().deferred.await()) println(repository.allTasksByUser(user3).fix().deferred.await()) } catch (e: UserNotInRemoteStorage) { println(e) } } } } } 

如您所知,必须明确规定使用corutin时的异常处理。 , , .


— :


 [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

Arrow API DeferredK . runBlocking :


 object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val deferredModuleAlt = Module(DeferredK.async()) deferredModuleAlt.run { println(repository.allTasksByUser(user1).fix().unsafeAttemptSync()) println(repository.allTasksByUser(user2).fix().unsafeAttemptSync()) println(repository.allTasksByUser(user3).fix().unsafeAttemptSync()) } } } 

[ Try ]({{ '/docs/arrow/core/try/ru' | relative_url }}) (.., Success Failure ).


 Success(value=[Task(value=LocalTask assigned to user1)]) Success(value=[Task(value=Remote Task assigned to user2)]) Failure(exception=UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))) 

, , IO .
IO , in/out , , .


 object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val ioModule = Module(IO.async()) ioModule.run { println(repository.allTasksByUser(user1).fix().attempt().unsafeRunSync()) println(repository.allTasksByUser(user2).fix().attempt().unsafeRunSync()) println(repository.allTasksByUser(user3).fix().attempt().unsafeRunSync()) } } } 

 Right(b=[Task(value=LocalTask assigned to user1)]) Right(b=[Task(value=Remote Task assigned to user2)]) Left(a=UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))) 

IO — . Either<L,R> ( ). , "" Either , "" , . Right(...) , , Left(...) .


.


, . , , , .


.


… ?


, , . .


  • : , (, ), — . , .


  • , . . () ( ) , .


  • (), , (). , .


  • , . , ( ).


  • , API . ( map , flatMap , fold , ). , , Kotlin, Arrow — .


  • DI ( ), .., DI " ". , , . DI, .., , .


  • , , . , .., , .




, .
, , , , .


, . — Twitter: @JorgeCastilloPR .


(, ) :



FP to the max John De Goes FpToTheMax.kt , arrow-examples . , , .

Source: https://habr.com/ru/post/zh-CN447234/


All Articles