
哈Ha!
我叫Artyom Dobrovinsky,我在
Finch工作。 我建议阅读
Arrow函数编程库之父之一的文章,内容涉及如何编写多态程序。 通常,刚开始以功能风格开始写作的人并不急于摒弃旧习惯,实际上,他们使用DI容器和继承编写了更优雅的命令。 无论使用哪种类型,重用函数的想法都可能促使许多人朝正确的方向思考。
好好享受
***
如果我们可以编写应用程序而不考虑运行时将使用的数据类型,而仅仅描述如何处理这些数据怎么办?
想象一下,我们有一个与RxJava库中的Observable类型兼容的应用程序。 这种类型使我们可以编写带有数据的调用和操作链,但是最后,这个Observable是否将不仅仅是具有附加属性的容器?
具有相同类型的故事,如Flowable, Deferred (协程), Future , IO以及许多其他类型。
从概念上讲,所有这些类型都代表一个操作(已经完成或计划在将来实现),该操作支持诸如将内部值转换为另一种类型( map )的操作,使用flatMap来创建相似类型的操作链,并结合相同类型的其他实例( zip )等
为了基于这些行为编写程序,同时保持声明性描述,并使程序独立于诸如Observable类的特定数据类型Observable使用的数据类型与某些协定(例如map , flatMap等)相对应Observable足够了。
这种方法可能看起来很奇怪或太复杂,但它具有有趣的优点。 首先,考虑一个简单的示例,然后再讨论它们。
规范问题
假设我们有一个带有待办事项列表的应用程序,并且我们想从本地缓存中提取Task类型的对象列表。 如果在本地存储中找不到它们,我们将尝试通过网络查询它们。 我们需要为这两个数据源提供一个合约,以便它们都可以为合适的User对象获得Task类型的对象列表,而与数据源无关:
 interface DataSource { fun allTasksByUser(user: User): Observable<List<Task>> } 
在这里,为简单起见,我们返回Observable ,但它可以是Single , Maybe , Flowable , Deferred -任何适合实现目标的东西。
添加一些数据源的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>这样的构造函数,其中F和A都可以是一个泛型类型。 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 {  
ApplicativeError从Applicative继承的,它们都是类型类。
类型类定义行为(合同)。 它们被编码为使用通用类型形式的参数的接口,例如Monad<F> , Functor<F>等。 此F是数据类型。 这样,我们可以传递诸如Either , Option , IO , Observable , Flowable等类型。
因此,回到我们的两个问题:
- 将函数成功完成后获得的值包装在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)的错误,其中t为Throwable ,因为我们将错误类型声明为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() ,即 创建可以从同步或异步代码(例如Thread或AsyncTask调用的操作。
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 . , , .