应用一系列返回可选值的函数到一个值的惯用函数式方式是什么?

huangapple go评论56阅读模式
英文:

What is the idiomatic functional way to apply a list of functions returning an optional successively to a value?

问题

以下是翻译好的部分:

如果我有一个条目 x 和一个函数列表 x -> Opt[x],那么应用这些函数以便依次获得最终的 Opt[x] 的惯用函数式编程方法是什么?

x -> [x->Opt[x]] -> Opt[x]

每个 x -> Opt[x] 都是一种过滤/丰富功能,可以添加内容到 x,或者如果它想要过滤 x,则不返回任何内容。

我了解通常的选择和列表单子以及它们的 map、apply 和 bind 函数,但我很难想出一种感觉像函数式编程的解决方案。

谢谢您提供正确方向的任何线索!

英文:

If i have an entry x and a list of functions x -> Opt[x], what is the idiomatic functional programming way to apply each of the functions successively to get a resulting Opt[x]?

x -> [x->Opt[x]] -> Opt[x]

Each x -> Opt[x] is some sort of filtering/enriching function, which can either add stuff to x or return nothing if it wants to filter x.

I know the usual suspects like Optional and List monads and their map, apply and bind functions, but i am having a hard time coming up with a solution which feels functional programming idiomatic.

Thank you for any clue into the right direction!

答案1

得分: 1

在Haskell术语中,您正在寻找foldM,其中折叠操作调用列表中的每个函数,因此 foldM (&) (= foldM (\x fn -> fn x))。

英文:

In Haskell terminology, you're looking for foldM where the folding operation calls each of your functions in the list, so foldM (&) (= foldM (\x fn -> fn x)).

答案2

得分: 0

Bergi的回答非常简单且有效,所以如果你使用Haskell或知道如何将foldM翻译成你选择的语言,那可能是最简单的方法。

如果你对这个问题有一个更通用的思考方式感兴趣,继续往下看。

我将把所需的函数翻译成Haskell。我假设

x -> [x->Opt[x]] -> Opt[x]

与Haskell中的

a -> [a -> Maybe a] -> Maybe a

是等价的。

我们可能会注意到,如果我们稍微重新排列一下这些术语,就会出现一种模式:

[a -> Maybe a] -> a -> Maybe a

由于柯里化,这相当于:

[a -> Maybe a] -> (a -> Maybe a)

括号是多余的,它们只是为了说明。继续教学线路一会儿,假设我们引入了一个类型别名:

type Foo a = a -> Maybe a

上面的类型现在是:

[Foo a] -> Foo a

所以现在的问题是如何将任意值的列表归约为单个值的可识别的抽象形式。我们知道答案:如何将任意值的列表归约为单个值的可识别的抽象形式。

如果列表中的值导致一个幺半群,那么这是可能的。特别是在Haskell中,这个函数是mconcat,它概括为fold

这很有趣!

现在的问题是,a -> Maybe a是否导致幺半群?

这个函数类型实际上是一个Kleisli箭头:Kleisli Maybe a a。是否有Kleisli箭头的Monoid实例?是的,至少有三个。

我们可以注意到,Kleisli Maybe a a看起来像一个自同态,因为有重复的a。虽然它不完全是一个自同态,但也许我们可以将它变成一个…

将带有颠倒参数的Monad绑定(=<<)会将类型为a -> Maybe a的函数foo转化为自同态:

ghci> :t (=<<) foo
(=<<) foo :: Integral b => Maybe b -> Maybe b

几乎完成了。现在用Endo包装它,使它成为Monoid实例:

ghci> :t Endo $ (=<<) foo
Endo $ (=<<) foo :: Integral a => Endo (Maybe a)

我们需要一些函数来玩:

projectEven i = if even i then Just i else Nothing
foo i = if i `mod` 10 == 0 then Just (i + 1) else Nothing

现在我们可以制作一个包含它们的列表:

ghci> :t [foo, projectEven]
[foo, projectEven] :: Integral a => [a -> Maybe a]

将它们都映射到那个Monoid实例:

ghci> :t fmap (Endo . (=<<)) [foo, projectEven]
fmap (Endo . (=<<)) [foo, projectEven]
  :: Integral a => [Endo (Maybe a)]

对它们进行fold

ghci> :t fold $ fmap (Endo . (=<<)) [foo, projectEven]
fold $ fmap (Endo . (=<<)) [foo, projectEven]
  :: Integral a => Endo (Maybe a)

并解封它:

ghci> :t appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]
appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]
  :: Integral a => Maybe a -> Maybe a

这是一个从Maybe aMaybe a的函数。由于我们想要a -> Maybe a,所以我们可以将它与Just组合,使“正常”的a变成Maybe a

ghci> :t (appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]) . Just
(appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]) . Just
  :: Integral a => a -> Maybe a

让我们将其制作成一个函数:

runFilters = (appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]) . Just

这只是一个普通的函数,你可以调用它:

ghci> runFilters 10
Just 11
ghci> runFilters 20
Just 21
ghci> runFilters 2
Nothing
ghci> runFilters 3
Nothing

显然,这不像foldM那么简单,但也许暗示着一些更深层次的抽象。

实际上,我在这里有点走火入魔,抱歉…

英文:

Bergi's answer is simple and works, so if you're using Haskell or know how to translate foldM to your language of choice, that's probably going to be the easiest way to do it.


If you're interested in a more generalisable way to think about this problem, however, read on.

I'm going to translate the desired function into Haskell. I'm assuming that

x -> [x->Opt[x]] -> Opt[x]

is the same as

a -> [a -> Maybe a] -> Maybe a

in Haskell.

The first thing we might notice is that a pattern starts to emerge if we rearrange the terms a bit:

[a -> Maybe a] -> a -> Maybe a

Because of currying, that is equivalent to:

[a -> Maybe a] -> (a -> Maybe a)

The brackets are redundant, so they're only for illustrative purposes. Continuing the didactic line for a moment, suppose we introduce a type alias:

type Foo a = a -> Maybe a

The above type would now be:

[Foo a] -> Foo a

So the question is now of a recognisable abstract form of which 'we' know the answer: How do we reduce an arbitrary list of values to a single value?

This is possible if the values in the list give rise to a monoid. Specifically in Haskell, this function is mconcat, which generalises to fold.

This is fun!

The question is, now, does a -> Maybe a give rise to a monoid?

That function type is really a Kleisli arrow: Kleisli Maybe a a. Is there a Monoid instance for Kleisli arrows? Yes, at least three.

We may notice that Kleisli Maybe a a looks like an endomorphism because of the repeated a. It's not quite an endomorphism, but perhaps we can make it one...

Monadic bind with the arguments flipped (=<<) will turn a function foo of the type a -> Maybe a into an endomorphism:

ghci> :t (=<<) foo
(=<<) foo :: Integral b => Maybe b -> Maybe b

Almost there. Now wrap it in Endo to make it a Monoid instance:

ghci> :t Endo $ (=<<) foo
Endo $ (=<<) foo :: Integral a => Endo (Maybe a)

We're going to need a few functions to play with:

projectEven i = if even i then Just i else Nothing
foo i = if i `mod` 10 == 0 then Just (i + 1) else Nothing

We can now make a list of these:

ghci> :t [foo, projectEven]
[foo, projectEven] :: Integral a => [a -> Maybe a]

Map them all to that Monoid instance:

ghci> :t fmap (Endo . (=<<)) [foo, projectEven]
fmap (Endo . (=<<)) [foo, projectEven]
  :: Integral a => [Endo (Maybe a)]

fold it:

ghci> :t fold $ fmap (Endo . (=<<)) [foo, projectEven]
fold $ fmap (Endo . (=<<)) [foo, projectEven]
  :: Integral a => Endo (Maybe a)

and unwrap it:

ghci> :t appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]
appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]
  :: Integral a => Maybe a -> Maybe a

That's a function from Maybe a to Maybe a. Since we want a -> Maybe a, we can compose it with Just so that the 'normal' a is lifted to Maybe a:

ghci> :t (appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]) . Just
(appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]) . Just
  :: Integral a => a -> Maybe a

Let's make that a function:

runFilters = (appEndo $ fold $ fmap (Endo . (=<<)) [foo, projectEven]) . Just

That's just a normal function that you can call:

ghci> runFilters 10
Just 11
ghci> runFilters 20
Just 21
ghci> runFilters 2
Nothing
ghci> runFilters 3
Nothing

Clearly, this isn't as easy as foldM, but may perhaps hint at some deeper abstractions at play.

Really, I just got a bit carried away here, sorry...

huangapple
  • 本文由 发表于 2023年2月23日 20:34:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/75544908.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定