同构急救

“同构”是现代数学的基本概念之一。 使用Haskell和C#中的具体示例,我不仅将为非数学家解释该理论(不使用任何晦涩的数学符号和术语),而且还将展示如何将其用于日常实践中。


问题在于严格的相等性(例如2 + 2 = 4)通常过于严格。 这是一个例子:


哈斯克尔
add :: (a, a) -> a add (x, y) = x + y 

C#
 int Add(Tuple<int, int> pair) { return pair.Item1 + pair.Item2; } 

但是, 从某种意义上讲 ,定义同一功能还有另一种方法(更棘手,在许多情况下更实用):


哈斯克尔
 add' :: a -> a -> a add' x = \y -> x + y 

C#
 Func<int, int> Add_(int x) { return y => x + y; } 

与显而易见的事实相反,对于任何两个x,y,两个函数将始终返回相同的结果,它们不满足严格的相等性:


  • 第一个函数立即返回金额(即在导出时执行计算),
  • 而第二个函数返回另一个函数(最后会返回总和-当然,如果有人调用它,否则将不执行任何计算:这是延迟计算的一个示例,这里也有一个同构,我将返回同构稍后)。

这是“太严格了”。


同构是“相当严格的”。 它不需要完全的,包罗万象的平等,而仅限于“某种意义上”的平等,它总是由特定的上下文决定。


您可能会猜到,以上两个定义都是同构的。 这恰好意味着:如果只给我一个,那么就暗含地给我:都归功于同构-一种双向转换为另一种方式 。 总结一下类型:


哈斯克尔
 curry :: ((a, b) → c) → a → b → c curry fxy = f (x, y), uncurry :: (a → b → c) → (a, b) → c uncurry f (x, y) = fxy 

C#
 Func<TArg1, Func<TArg2, TRes>> Curry(Func<Tuple<TArg1, TArg2>, TRes> uncurried) { return arg1 => arg2 => uncurried(Tuple.Create(arg1, arg2)); } Func<Tuple<TArg1, TArg2>, TRes> Uncurry(Func<TArg1, Func<TArg2, TRes>> curried) { return pair => curried(pair.Item1)(pair.Item2); } 

...现在对于任何x,y


哈斯克尔
 curry add $ x, y = uncurry add' $ (x, y) 

C#
 Curry(Add)(x)(y) = Uncurry(Add_)(Tuple.Create(x, y)) 

特别好奇的数学知识

实际上,它应如下所示:


哈斯克尔
 curry . uncurry = id uncurry . curry = id id x = x 

C#
 Compose(Curry, Uncurry) = Id Compose(Uncurry, Curry) = Id, : T Id<T>(T arg) => arg; Func<TArg, TFinalRes> Compose<TArg, TRes, TFinalRes>( Func<TArg, TRes> first, Func<TRes, TFinalRes> second) { return arg => second(first(arg)); } ...  extension- (  Id   ): Curry.Compose(Uncurry) = Id Uncurry.Compose(Curry) = Id, : public static Func<TArg, TFinalRes> Compose<TArg, TRes, TFinalRes>( this Func<TArg, TRes> first, Func<TRes, TFinalRes> second) { return arg => second(first(arg)); } 

Id应该理解为“什么都没有发生”。 根据定义,由于同构是双向转换器,因此您始终可以:1)处理一件事情,2)将其转换为另一件,3)将其转换回第一件。 这样的操作只有两个:因为在第一阶段(第1步),只有两个选项可供选择。 在这两种情况下,该操作应导致完全相同的结果,好像什么都没有发生(因此,涉及严格的相等性-因为什么都没有改变,而“某物”没有改变)。


除此之外,还有一个定理,id元素总是唯一的。 请注意,Id函数是通用的,多态的,因此对于每种特定类型而言,它确实是唯一的。


同构非常有用,正因为它很严格,但又不过分。 它保留了某些重要属性(在上面的示例中-具有相同参数的相同结果),同时允许您自由转换数据结构本身(同构行为和属性的载体)。 这绝对是安全的-因为同构总是在两个方向上起作用,这意味着您可以始终返回而不会丢失那些“重要属性”。 我将再举一个在实践中非常有用的示例,它甚至可以成为Haskell的许多“高级”编程语言的基础:


哈斯克尔
 toLazy :: a -> () -> a toLazy x = \_ -> a fromLazy :: (() -> a) -> a fromLazy f = f () 

C#
 Func<TRes> Lazy(TRes res) { return () => res; } TRes Lazy(Func<TRes> lazy) { return lazy(); } 

这种同构性保留了延迟计算本身的结果-这是“重要属性”,而数据结构不同。


结论呢? OOP(特别是强类型的)(强制)在“严格平等”级别上工作。 因此,在上述示例之后,它通常过于严格。 当您习惯于“过于严格地”思考(这种情况难以察觉地发生-泄漏到程序员那里,特别是如果他不希望在数学中寻求灵感)时,您的决策会无意间失去其期望的(或至少客观上可能的)灵活性。 了解同构性-在一个有意识地尝试更注意自己的社区中
以及外来代码-它有助于从不必要的细节中抽象出来,从而更清楚地定义“重要属性”的圈:即从上面印有这些“重要属性”的特定数据结构(就此而言,它们也是“实现细节”)。 首先,这是一种思考方式,然后才是-更成功的(微)体系结构解决方案,以及自然而然的是经过修订的测试方法。


PS:如果我看到这篇文章有所帮助,那么我将回到“更成功的(微)体系结构解决方案”和“修订的测试方法”主题。

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


All Articles