以 pointfree 风格编写函数的一般方案是什么?

发布于 2024-12-23 09:47:57 字数 1720 浏览 5 评论 0原文

我现在正在做20个中级Haskell练习,这是一个非常有趣的练习。它涉及实现类型类 FunctorMonad 的各种实例(以及将 FunctorMonad 作为参数的函数)参数),但使用像 FurryMisty 这样可爱的名字来掩盖我们正在做的事情(产生一些有趣的代码)。

我一直在尝试以无点风格来做其中的一些事情,我想知道是否有一个通用方案可以将有点(?)定义转变为无点定义。例如,以下是 Misty 的类型类:(

class Misty m where
  unicorn :: a -> m a
  banana :: (a -> m b) -> m a -> m b

函数 unicornbanana 分别是 return>>=,以防不明显)这是我对 apple 的实现(相当于 flip ap):

apple :: (Misty m) => m a -> m (a -> b) -> m b
apple x f = banana (\g -> banana (unicorn . g) x) f

练习的后面部分让你实施版本liftMliftM2 等。这是我的解决方案:

appleTurnover :: (Misty m) => m (a -> b) -> m a -> m b
appleTurnover = flip apple

banana1 :: (Misty m) => (a -> b) -> m a -> m b
banana1 =  appleTurnover . unicorn

banana2 :: (Misty m) => (a -> b -> c) -> m a -> m b -> m c
banana2 f = appleTurnover . banana1 f

banana3 :: (Misty m) => (a -> b -> c -> d) -> m a -> m b -> m c -> m d
banana3 f x = appleTurnover . banana2 f x

banana4 :: (Misty m) => (a -> b -> c -> d -> e) -> m a -> m b -> m c -> m d -> m e
banana4 f x y = appleTurnover . banana3 f x y

现在,banana1 (相当于 liftMfmap)我能够通过 appleTurnover 的适当定义以 pointfree 风格实现。但对于其他三个函数,我必须使用参数。

我的问题是:是否有办法将此类定义转变为无点定义

I am working through the 20 Intermediate Haskell Exercises at the moment, which is quite a fun exercise. It involves implementing various instances of the typeclasses Functor and Monad (and functions that takes Functors and Monads as arguments) but with cute names like Furry and Misty to disguise what we're doing (makes for some interesting code).

I've been trying to do some of this in a point-free style, and I wondered if there's a general scheme for turning a point-ful (?) definition into a point-free definition. For example, here is the typeclass for Misty:

class Misty m where
  unicorn :: a -> m a
  banana :: (a -> m b) -> m a -> m b

(the functions unicorn and banana are return and >>=, in case it's not obvious) and here's my implementation of apple (equivalent to flip ap):

apple :: (Misty m) => m a -> m (a -> b) -> m b
apple x f = banana (\g -> banana (unicorn . g) x) f

Later parts of the exercises have you implement versions of liftM, liftM2 etc. Here are my solutions:

appleTurnover :: (Misty m) => m (a -> b) -> m a -> m b
appleTurnover = flip apple

banana1 :: (Misty m) => (a -> b) -> m a -> m b
banana1 =  appleTurnover . unicorn

banana2 :: (Misty m) => (a -> b -> c) -> m a -> m b -> m c
banana2 f = appleTurnover . banana1 f

banana3 :: (Misty m) => (a -> b -> c -> d) -> m a -> m b -> m c -> m d
banana3 f x = appleTurnover . banana2 f x

banana4 :: (Misty m) => (a -> b -> c -> d -> e) -> m a -> m b -> m c -> m d -> m e
banana4 f x y = appleTurnover . banana3 f x y

Now, banana1 (equivalent to liftM or fmap) I was able to implement in pointfree style, by a suitable definition of appleTurnover. But with the other three functions I've had to use parameters.

My question is: is there a recipe for turning definitions like these into point-free definitions?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

踏雪无痕 2024-12-30 09:47:57

正如 pointfree 实用程序所演示的,可以自动执行任何此类转换。然而,结果往往是混乱的而不是改进的。如果一个人的目标是增强易读性而不是破坏它,那么第一个目标应该是确定为什么一个表达式具有特定的结构,找到一个合适的抽象,并以这种方式构建事物。

最简单的结构是将事物简单地链接在线性管道中,这是简单的函数组合。这本身就让我们走得很远,但正如你所注意到的,它并不能处理所有事情。

一种概括是带有附加参数的函数,这些参数可以逐步构建。下面是一个示例:定义 onResult = (.(.))。现在,将 onResult 应用到 id 初始值 n 次,即可获得具有 n 元函数结果的函数组合。所以我们可以定义comp2 = onResult(.),然后写comp2 not (&&)来定义一个NAND操作。

另一个概括(实际上包含上述内容)是定义将函数应用于较大值的组件的运算符。这里的一个例子是 Control.Arrow 中的 firstsecond,它们适用于 2 元组。 Conal Elliott 的语义编辑器组合器基于这种方法。

稍微不同的情况是,当您在某种类型 b 上有一个多参数函数,并且有一个函数 a ->; b,并且需要使用a将它们组合成一个多参数函数。对于二元函数的常见情况,模块 Data.Function 提供了 on 组合器,您可以使用它来编写诸如 compare `on` fst 之类的表达式 比较 2 元组的第一个元素。

当单个参数被多次使用时,这是一个更棘手的问题,但这里也可以提取有意义的重复模式。这里的常见情况是将多个函数应用于单个参数,然后使用另一个函数收集结果。这恰好对应于函数的 Applicative 实例,它允许我们编写像 (&&) <$> 这样的表达式。 (>3)<*> (<9) 检查数字是否在给定范围内。

如果您想在实际代码中使用其中任何一个,重要的是要考虑表达式的含义以及它如何在结构中反映。如果您这样做,然后使用有意义的组合器将其重构为 pointfree 样式,您通常会使代码的意图比其他方式更清晰,这与 pointfree< 的典型输出不同/代码>。

As demonstrated by the pointfree utility, it's possible to do any such conversion automatically. However, the result is more often obfuscated than improved. If one's goal is to enhance legibility rather than destroy it, then the first goal should be to identify why an expression has a particular structure, find a suitable abstraction, and build things up that way.

The simplest structure is simply chaining things together in a linear pipeline, which is plain function composition. This gets us pretty far just on its own, but as you noticed it doesn't handle everything.

One generalization is to functions with additional arguments, which can be built up incrementally. Here's one example: Define onResult = (. (.)). Now, applying onResult n times to an initial value of id gives you function composition with the result of an n-ary function. So we can define comp2 = onResult (.), and then write comp2 not (&&) to define a NAND operation.

Another generalization--which encompasses the above, really--is to define operators that apply a function to a component of a larger value. An example here would be first and second in Control.Arrow, which work on 2-tuples. Conal Elliott's Semantic Editor Combinators are based on this approach.

A slightly different case is when you have a multi-argument function on some type b, and a function a -> b, and need to combine them into a multi-argument function using a. For the common case of 2-ary functions, the module Data.Function provides the on combinator, which you can use to write expressions like compare `on` fst to compare 2-tuples on their first elements.

It's a trickier issue when a single argument is used more than once, but there are meaningful recurring patterns here that can also be extracted. A common case here is applying multiple functions to a single argument, then collecting the results with another function. This happens to correspond to the Applicative instance for functions, which lets us write expressions like (&&) <$> (> 3) <*> (< 9) to check if a number falls in a given range.

The important thing, if you want to use any of this in actual code, is to think about what the expression means and how that's reflected in the structure. If you do that, and then refactor it into pointfree style using meaningful combinators, you'll often make the intent of the code clearer than it would otherwise be, unlike the typical output of pointfree.

梦太阳 2024-12-30 09:47:57

是的!技巧之一是用前缀表示法而不是中缀来写点。然后你应该能够找到看起来像函数组合的新东西。下面是一个示例:

banana2 f = appleTurnover . banana1 f
          = (.) appleTurnover (banana1 f)
          = ((.) appleTurnOver) . banana1 $ f
banana2 = (appleTurnover .) . banana1

pointfree 实用程序的源代码包含更多内容,但这个可以处理很多情况。

banana4 f x y = appleTurnover . banana3 f x y
              = (.) appleTurnover ((banana3 f x) y)
              = ((.) appleTurnover) . (banana3 f x) $ y
banana4 f x = ((.) appleTurnover) . (banana3 f x)
            = (.) ((.) appleTurnover) (banana3 f x)
            = ((.) ((.) appleTurnover)) ((banana3 f) x)
            = ((.) ((.) appleTurnover)) . (banana3 f) $ x
banana4 f = ((.) ((.) appleTurnover)) . (banana3 f)
          = (.) ((.) ((.) appleTurnover)) (banana3 f)
          = ((.) ((.) ((.) appleTurnover))) (banana3 f)
          = ((.) ((.) ((.) appleTurnover))) . banana3 $ f
banana4 = ((.) ((.) ((.) appleTurnover))) . banana3
        = (((appleTurnover .) .) .) . banana3

Yes! One of the tricks is to write your dots in prefix notation rather than infix. Then you should be able to find new things that look like function composition. Here's an example:

banana2 f = appleTurnover . banana1 f
          = (.) appleTurnover (banana1 f)
          = ((.) appleTurnOver) . banana1 $ f
banana2 = (appleTurnover .) . banana1

The source code for the pointfree utility contains more, but this one handles a lot of cases.

banana4 f x y = appleTurnover . banana3 f x y
              = (.) appleTurnover ((banana3 f x) y)
              = ((.) appleTurnover) . (banana3 f x) $ y
banana4 f x = ((.) appleTurnover) . (banana3 f x)
            = (.) ((.) appleTurnover) (banana3 f x)
            = ((.) ((.) appleTurnover)) ((banana3 f) x)
            = ((.) ((.) appleTurnover)) . (banana3 f) $ x
banana4 f = ((.) ((.) appleTurnover)) . (banana3 f)
          = (.) ((.) ((.) appleTurnover)) (banana3 f)
          = ((.) ((.) ((.) appleTurnover))) (banana3 f)
          = ((.) ((.) ((.) appleTurnover))) . banana3 $ f
banana4 = ((.) ((.) ((.) appleTurnover))) . banana3
        = (((appleTurnover .) .) .) . banana3
昵称有卵用 2024-12-30 09:47:57

我使用以下术语重写系统:

\x -> f x ------> f 
f y x ----------> flip f x y
\x -> f (g x) --> f . g

它不完整(请阅读有关组合逻辑的书籍中的原因),但它已经足够了:

这是banana2:

banana2 f = appleTurnover . banana1 f

重写为lambda:

banana2 = \f -> appleTurnover . banana1 f

以前缀样式写入(.):

banana2 = \f -> (.) appleTurnover (banana1 f)

请注意,

banana2 = \f -> ((.) appleTurnover) (banana1 f)

因此可以应用规则3 。 f(.) appleTurnovergbanana

banana2 = ((.) appleTurnover) . banana1

I use the following term rewrite system:

\x -> f x ------> f 
f y x ----------> flip f x y
\x -> f (g x) --> f . g

It is incomplete (read why in books about combinatory logic), but it's enough:

Here is banana2:

banana2 f = appleTurnover . banana1 f

Rewrite as a lambda:

banana2 = \f -> appleTurnover . banana1 f

Write (.) in prefix style:

banana2 = \f -> (.) appleTurnover (banana1 f)

Note that

banana2 = \f -> ((.) appleTurnover) (banana1 f)

So rule 3 can be applied. f is (.) appleTurnover and g is banana:

banana2 = ((.) appleTurnover) . banana1
冰葑 2024-12-30 09:47:57

有一个 pointfree 包,它采用 Haskell 函数定义并尝试以 pointfree 风格重写它。我建议尝试一下以获得新想法。请参阅此页面了解更多详细信息;该软件包位于此处

There is a pointfree package which takes a Haskell function definition and attempts to re-write it in a pointfree style. I'd suggest experimenting with it to get new ideas. See this page for more details; the package is available here.

随心而道 2024-12-30 09:47:57

由于 pointfree 风格是组合器风格,只需应用已知的组合器定义,向后读取它们即可进行替换:

B f g x = f (g x)     -- (.) , <
gt; for ((->) a)
C f x y = f y x       -- flip
K x y = x             -- const
I x = x               -- id
S f g x = f x (g x)   -- <*> , ap  for ((->) a)
W f x = f x x         -- join
(f >>= g) x = g (f x) x
(f =<< g) x = f (g x) x

有时 liftMxliftAxsequencesequenceA 可以简化事情。我还会将 foldrunfoldriterateuntil 等视为基本组合器。

通常,使用运算符部分也有帮助:

op a b = (a `op` b) = (`op` b) a = (a `op`) b

某些模式可能会变得熟悉,因此可以直接使用:

((f .) . g) x y = f (g x y)
((. f) . g) x y = g x (f y)

(((f .) .) . g) x y z = (f .) (g x y) z = f (g x y z)
(((. f) .) . g) x y z = (. f) (g x y) z = g x y (f z)

等等。

Since pointfree style is combinators style, just apply known combinators definitions, reading them backwards to make the substitution:

B f g x = f (g x)     -- (.) , <
gt; for ((->) a)
C f x y = f y x       -- flip
K x y = x             -- const
I x = x               -- id
S f g x = f x (g x)   -- <*> , ap  for ((->) a)
W f x = f x x         -- join
(f >>= g) x = g (f x) x
(f =<< g) x = f (g x) x

At times liftMx, liftAx, sequence, sequenceA can simplify things. I'd also consider foldr, unfoldr, iterate, until etc. as basic combinators.

Often, using operator sections helps too:

op a b = (a `op` b) = (`op` b) a = (a `op`) b

Some patterns can become familiar and so, used directly:

((f .) . g) x y = f (g x y)
((. f) . g) x y = g x (f y)

(((f .) .) . g) x y z = (f .) (g x y) z = f (g x y z)
(((. f) .) . g) x y z = (. f) (g x y) z = g x y (f z)

etc.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文