功能实践和前端:monad和函子

大家好! 我叫Dmitry Rudnev,我是BCS的前端开发人员。 我以布局复杂程度不同的界面开始了自己的旅程,并始终特别注意界面:如果我可以按照设计者的意图将界面传达给用户,那么用户与他进行交互的舒适度就很高。



在本系列文章中,我想分享我在前端开发中应用功能实践的经验,我将讨论您作为使用这些实践的开发人员所能获得的利弊。 如果您喜欢该主题,那么我们将陷入功能世界的“干燥者”和更复杂的角落。 我立即注意到,我们将从大到小,即从鸟瞰的角度看待经典应用程序,并且在阅读本文时,我们将深入研究具体实践将给我们带来明显好处的地方。

因此,让我们从处理状态开始。 同时,我将同时告诉您单子和仿函数。

前言


在展开下一个界面并在UI和分析之间找到共同点时,我开始注意到,每次开发人员与网络打交道时,他只需要处理所有UI状态并描述对特定状态的反应。 而且由于我们每个人都在追求卓越,因此希望有一种处理状态的方式,以尽可能透明地描述正在发生的事情,什么是特定反应的发起者,以及结果是工作的结果。 幸运的是,在编程世界中,几乎所有您能想到的东西都是由您之前的人实现的。

在开发领域和设计领域中,不仅形成了可以有效解决问题的模式,而且还形成了反模式,因此应避免使用反模式,以免不良做法泛滥成灾,并且开发人员或设计人员在各种情况下始终立于不败之地,没有具体解决方案时。

在我们的案例中,大多数开发人员拥有的情况是处理UI元素的所有状态并对它们做出反应。 这里的问题是UI元素既可以与本地状态(不执行异步请求)交互,也可以与远程资源或存储库交互。 开发人员有时会忘记处理所有极端情况,这会导致整个系统的行为不一致。

所有示例都将包含使用React库和JavaScript超集-TypeScript的代码示例,以及用于功能编程fp-ts的库。

考虑最简单的示例,其中有一个我们从服务器请求的元素列表,我们需要根据请求的结果正确显示UI。 我们对render函数很感兴趣,因为在执行请求期间,我们需要在其中显示正确的状态。 完整的示例代码可以在以下位置查看: simple application 。 将来,将有一个完整的项目,侧重于一系列文章,在此过程中,我们将分解其各个部分。

  const renderInitial = (...) => ...; const renderPending = (...) => ...; const renderError = (...) => ... ; const renderSuccess = (...) => ... ; return ( {state.subcribers.foldL( renderInitial, renderPending, renderError, renderSuccess, )} ); 

该示例清楚地表明,数据模型的每个状态都有其自己的功能,并且每个功能都返回为其指定的UI片段(消息,按钮等)。 展望未来,我将说该示例使用RemoteData monad

那是如此优雅,最重要的是安全,我们可以处理数据并对其进行响应。 这是引言,我试图在一个看似简单的示例中演示功能方法的好处。

函子和Monad


现在,让我们开始逐步研究类别的应用理论,并分析诸如FunctorMonad之类的概念,并考虑使用功能性实践安全处理数据的实践。

“实质上,函子只不过是一种数据结构,它使您可以应用转换函数从shell中提取值,对其进行修改,然后将其放回shell中。

将值包含在外壳或容器中是功能编程中的基本设计模式,因为它可以防止直接访问值,从而可以在应用程序中安全,不变地对其进行操作。”

我从一本精彩的书中引用了这段话, 该书回顾了JavaScript中的函数编程技术 。 让我们从理论成分入手,分析一下函子实际上是什么。 首先,我们需要在最基本的层次上熟悉数学的一个有趣部分,即类别理论。

范畴理论是数学的一个分支,研究数学对象之间的关系特性,而与对象的内部结构无关。 范畴论在现代数学中占据着中心位置;它还发现了在计算机科学,逻辑学和理论物理学中的应用。

类别由对象和在它们之间定向的箭头组成。 可视化类别的最简单方法是:

排列箭头的方式是,如果您有一个从对象A到对象B的箭头,以及一个从对象BC的箭头,则必须有一个箭头-它们的组成是从AC。 将箭头视为功能; 他们也被称为态射。 您有一个函数f以A作为参数并返回B。还有另一个函数g以B作为参数并返回C。您可以通过将结果从f传递到g来组合它们。 我们刚刚描述了一个新函数,该函数取A并返回C。在数学中,这种表示形式由函数表示法之间的小圆圈表示:g◦f。 注意构图顺序-从右到左。

在数学中,合成是从右到左。 在这种情况下,如果将g◦f读为“ f之后的g”,将很有帮助。

 -—   A  B f :: A -> B -—   B   g :: B -> C -— A  C g . f 

任何类别的组合物都必须满足两个非常重要的属性。

  1. 组合是关联的(关联性是一种操作的属性,允许您在没有相同优先级的显式继承指示的情况下恢复执行顺序;这区分了左关联性(从左到右评估表达式)和右关联性(从右到左)。相应的运算符称为左关联和右关联如果您可以排列三个变态(箭头)f,g和h(即它们的类型彼此一致),则您 需要括号将它们分组。数学上,这被写成h ◦ (g ◦ f) = (h ◦ g) ◦ f = h ◦ g ◦ f (H◦克)◦F =ħ◦克◦˚F
  2. 对于每个对象A,都有一个箭头,它将是一个组成单位。 该箭头是从对象到自身的。 成为合成单位意味着用一个分别以A开头或以A结束的箭头组成一个单位时,合成将返回相同的箭头。 对象A的单位箭头称为IDa(A上的单位)。 用数学符号表示,如果f从A到B,则f ◦ idA = f

    要使用函数,将单个箭头实现为相同的函数,只需返回其参数即可。

现在我们可以考虑类别理论中的函子。

函子是类别之间的特殊映射类型。 可以理解为保留结构的显示器。 小类别之间的函子是小类别类别中的态射。 通常,所有类别的总和不是类别,因为其对象的总和不是类别。 - 维基百科

考虑Maybe容器的仿函数的示例实现,这是“可能丢失的值”的想法。

 const compose = <A, B, C>( f: (a: A) => B, g: (b: B) => C, ): (a: A) => C => (a: A) => g(f(a)); //  Maybe: type Nothing = Readonly<{ tag: 'Nothing' }>; type Just<A> = Readonly<{ tag: 'Just'; value: A }>; export type Maybe<A> = Nothing | Just<A>; const nothing: Nothing = { tag: 'Nothing' }; const just = <A>(value: A): Just<A> => ({ tag: 'Just', value }); //    Maybe: const fmap = <A, B>(f: (a: A) => B) => (fa: Maybe<A>): Maybe<B> => { switch (fa.tag) { case 'Nothing': return nothing; case 'Just': return just(f(fa.value)); } }; //  1: fmap id === id namespace Laws { console.log( fmap(id)(just(42)), id(just(42)), ); // => { tag: 'Just', value: 42 } //  2: fmap f ◦ fmap g === fmap (f ◦ g) const f = (a: number): string => `Got ${a}!`; const g = (s: string): number => s.length; console.log( compose(fmap(f), fmap(g))(just(42)), fmap(compose(f, g))(just(42)), ); // => { tag: 'Just', value: 7 } } 

fmap方法可以从两个侧面查看:

  1. 作为将纯函数应用于“容器化”值的一种方式;
  2. 作为“提升到容器环境中”的一种方式,使用纯函数。

的确,如果接口中的括号稍有不同,我们可以得到fmap函数的签名:

 const fmap: <A, B>(f: (a: A) => B) => ((ma: Maybe<A>) => Maybe<B>); 

定义了接口:

 type Function1<Domain, Codomain> = (a: Domain) => Codomain; 

我们得到fmap的定义:

 const fmap: <A, B>(f: (a: A) => B) => Function1<Maybe<A>, Maybe<B>>; 

这个简单的技巧使我们可以将函子视为“将纯函数提升到容器上下文中”的一种方式。 因此,可以以一种安全的方式处理各种数据:例如,成功处理可选嵌套值的链; 转换数据清单 处理异常等。

如前所述,使用函子可以安全且不变地将函数应用于值。 Monad与函子相似,不同之处在于它们能够在特殊情况下委派特殊逻辑。 函子本人仅知道如何应用此功能并将结果包装回Shell中,并且他没有其他逻辑。

当通过从外壳程序中提取值并从嵌套中定义规则的原理来提取数据来创建整个数据类型时,就会出现monad。 像函子一样,monad是一种设计模板,用于以阶段顺序的形式描述计算,其中完全不知道已处理的值,但是当monad用于合成时,可以安全且无副作用地控制数据流。 Monads可以解决各种问题。 从理论上讲,单子依赖于特定语言的类型系统。 实际上,许多人认为只有在存在明确的数据类型时才能理解它们。

为了更好地理解monad,必须学习以下重要概念。
莫纳德 为单子运算提供抽象接口
单声道类型。 该接口的具体实现

但是,我将在以后的文章中介绍使用函子和其他分类构造的这些属性的实际示例。

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


All Articles