
哈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
. , , .