面向结果的编程:使用应用函子逐步生成结果

在我以前的故事中,我们探讨了如何使用函数组合逐步构建结果。 我们构建了applyResult函数的三个变体。 将applyResultflatMap可以使我们采用以下代码:

  func getTweetDetails(for userId:String) 
->结果 {
var用户:用户!
var tweet:鸣叫!

返回getUser(with:userId)
.flatMap {userData-> Result 在
用户=用户数据
返回getLatestTweet(for:userData)
} .flatMap {
tweetData->结果在
tweet = tweetData
返回getTweetSentiment(for:tweetData)
} .flatMap {sentimentData
.success(TweetDetails(用户,tweet,sentimentData))
}
}

并通过以下方式对其进行改进:

  func getTweetDetails(for userId:String)->结果 { 
让tweetDetails = curry(TweetDetails.init)
返回applyResult(of:getUser(),to:tweetDetails)
.flatMap(applyResult(of:getLatestTweet))
.flatMap(applyResult(of:getTweetSentiment))
}

一个好朋友和一位杰出的工程师最近观看了Stephen Celis的一段名为Applicatives and Swift的视频。 阅读我的上一篇文章后,他建议我观看视频。 他认为,通过使用可施加的函子,可以进一步简化解决方案。

我必须承认,在完全掌握这一概念之前,我不得不看了几次视频。 这是一个非常棒的视频,我强烈推荐该视频,即使仅观看Stephen Celis现场直播整个过程。

在Playgrounds中进行实验后,我能够将上述内容简化为非常优雅的内容:

  func getTweetDetails(for userId:String)->结果 { 
返回pure(createTweetDetails)
getUser()
pure(getLatestTweet)
pure(getTweetSentiment)
}

这篇文章将详细说明我如何使用应用函子实现上述目标。

在开始之前,让我们首先讨论应用程序。 如果我们看一下在“结果类型”中实现的map (functor)和flatmap (monad)的签名,我们将得到以下内容:

  (((A-> B),结果)->结果 
(((A->结果),结果
)->结果

我不会讨论函子或monad,因为这篇文章在做到这一点上做得非常好。 但是,现在通过在上下文中包装我们的函数,可应用函子只是扩展了函子。 这为我们提供了适用于函子的以下签名:

  (结果 B),错误>,结果)->结果 

乍看起来似乎不是最有用,但让我们首先实现该应用程序,然后使用它来显示其实用性。

我们将首先创建操作符:

 优先组LeftAssociativity { 
关联性:左
}
中缀运算符:LeftAssociativity

现在让我们创建一个称为pure的简单函数。 该函数将获取一个值并将其包装在Result

  func pure (_ x:A)->结果 { 
返回.success(x)
}

现在是实际的应用实现:

  func  (f:结果 B,E>,x:结果)->结果 { 
返回f.flatMap(x.map)
}

通过创建以下struct让我们将以上内容用于测试运行:

  struct Tweet { 
让ID:诠释?
让tweet:字符串
}

现在让我们创建一个简单的curried函数,以递增方式构建Tweet

 让createTweet = {id in {tweet in return Tweet(id:id,tweet:tweet)}} 

最后一步是为我们的输入创建两个简单的验证函数:

  func validateId(_ id:Int)-> Result  { 
返回ID> 0
? 纯(id)
:.failure(.😩(“无效ID”))
}
func validateTweet(_ tweet:String)-> Result {
返回tweet.characters.count <140
? 纯(tweet)
:.failure(.😩(“ tweet too long”))
}

最后,我们最终获得以下功能:

 让tweet = pure(createTweet) 
validateId(1)
validateTweet(“ Hello Twitter”)

太棒了

尝试传递无效的ID或超过140个字符的推文。 我们免费提供了一个失败的初始化器。 希望您能对为什么应用程序如此有用的想法有所了解。

如果您想使用可行的解决方案,请查看以下游乐场文件。

TYRONEMICHAEL /作物功能组合

rop-functional-composition —面向结果的编程:使用函数组合逐步生成结果

github.com

让我们看一下下面的代码,并确定如何满足这些条件:

 让createTweetDetails = { 
{中的用户
在{
感悟
TweetDetails(
用户:用户,
鸣叫:鸣叫,
情绪:情绪

}
}
}

因此,我们首先需要通过获取当前用户来执行功能。 然后,我们将使用用户的数据 来获取他们的最新推文 。 最终,我们将使用推文的数据 来获取 推文的情绪 。 我们在这样做的同时仍保留了铁路定向编程模式。 如果先前的任何功能失败,错误就会消失并且以下功能将不会执行。

因此,不幸的是,如果我们具有以下功能签名:

  func getUser()-> Result  
func getLatestTweet(用于用户:User?)-> Result
func getTweetSentiment(for tweet:Tweet?)->结果

我们之前的应用实现需要满足以下条件:

 让用户:Result  = 
pure(createTweetDetails)
getUser()
getLatestTweet(用于:用户)
getTweetSentiment(用于:Tweet)

更改函数签名不是一个选择,因此就像我们创建了applyResult不同变体一样,我们可以创建应用程序的不同变体。 和我在一起,因为起初可能会造成一些混乱。

那么,我们需要做什么?

让我们分解一下:

  • 我们首先需要将创建函数包装在Result
  • 然后,我们执行一个返回Result的函数。
  • 然后,我们的应用程序将解开Result以确定其类型是否为.success.failure 。 如果是后者,则我们什么也不做,错误就会通过。 如果是成功的Result ,则将未包装的值传递给我们的创建函数,以递增方式构建它。
  • 最后,我们将解开的值传递给下一个函数,再次执行前面的步骤,直到获得完整的模型。
  • 请记住,如果在任何阶段出现.failure ,以下函数将不会被执行,而我们会通过失败点。

哇 听起来很多。 但这就像把拼图拼在一起。 由于Swift是一种静态语言,我们需要编译器来帮助我们。 我们根本无法将错误的拼图拼在一起。

继续。 让我们看一下应用程序的当前实现:

  func  (f:结果 B,EmojiError>,x:结果)->结果 { 
开关(f,x){
大小写(.success(f),_):返回x.map(f)
case let(.failure(e),.success):返回.failure(e)
case let(.failure(_),.failure(e)):返回.failure(e)
}
}

现在看下面的代码:

 让结果:Result (TweetSentiment)-> TweetDetails,EmojiError> = pure(createTweetDetails) repo.getUser() 

我们的createTweetDetails需要一条Tweet 。 执行getLatestTweet()将返回Tweet。 不幸的是, getLatestTweet (这是我们需要执行的下一个功能)也需要一个User 。 我们可以通过取一个元组并将其包装在Result

 让结果:结果<( 
(Tweet)->(TweetSentiment)-> TweetDetails,用户,EmojiError> = pure(createTweetDetails) repo.getUser()

然后,我们可以将以前的函数返回的值传递给我们要执行的下一个函数。 我们通过应用程序的另一种实现方式进行此操作:

  func  (f:结果 B,EmojiError>,x:结果)->结果 { 
开关(f,x){
大小写(.success(r1),.success(r2)):
返回x.map {v in(r1(v),r2)}
大小写(.failure(e),.success):
返回.failure(e)
大小写(_,.failure(e)):
返回.failure(e)
}
}

最后,我们需要为应用程序创建另一个函数,该函数将接受返回结果的函数。 应用程序将拆开元组并将第二个值传递给函数。 最后,展开函数的结果并将其传递给createTweetDetails

  func  (f:结果 C,A),EmojiError>,x:结果 Result ,EmojiError>)- >结果 { 
开关(f,x){
大小写(.success(r1),.success(r2)):
返回r2(r1.1).map {v in(r1.0(v),v)}
大小写(.failure(e),.success):
返回.failure(e)
大小写(_,.failure(e)):
返回.failure(e)
}
}

我们希望返回一个Result而不将先前的值传递给下一个函数,因为我们的TweetDetails模型将是完整的。 这只是上面的内容,没有返回元组:

  func  (f:结果 C,A),EmojiError>,x:结果 Result ,EmojiError>)- >结果 { 
开关(f,x){
大小写(.success(r1),.success(r2)):
返回r2(r1.1).map {v in r1.0(v)}
大小写(.failure(e),.success):
返回.failure(e)
大小写(_,.failure(e)):
返回.failure(e)
}
}

最后,我们得出以下结论:

  func getTweetDetails(for userId:String)->结果 { 
返回pure(createTweetDetails)
getUser()
pure(getLatestTweet)
pure(getTweetSentiment)
}

我知道以上内容可以为您带来很多帮助。 但是,它变得越来越容易。 上面的例子似乎有些琐碎,但它完美地说明了在使用应用程序时我们如何遵循“ 铁路定向编程”模式。

斯蒂芬·塞利斯(Stephen Celis)很友善,可以读我的文章。 他指出了以下几点:

我最终了解到的主要事情是,当发送给函数的值都是独立的时,应用程序就会发光。 在您的示例中,在发出tweet请求之前,我们需要用户请求的结果,因此我们无法并行运行它们。

换句话说,对于上述实现方式,应用程序不是最佳选择。 累积错误时,应用程序会发光。 Monad具有应用程序所没有的限制:即,它们是面向铁路的,并且当Monad将轨道切换到错误情况时,它们就完成了。

实验虽然很有趣,但在整个过程中让资深人士帮助您很有帮助。

您可以在以下存储库中找到所有工作代码。 比上面的摘录更容易理解。

TYRONEMICHAEL /作物功能组合

rop-functional-composition —面向结果的编程:使用函数组合逐步生成结果

github.com


泰隆·阿芙妮(@tyroneavnit)| 推特

Tyrone Avnit(@tyroneavnit)的最新推文。 爸爸,丈夫和多语言程序员。 热情的利物浦…

twitter.com