寻求在Elixir中“发送+更多=金钱”的惯用,优雅的解决方案

时间:2015-08-05 04:07:10

标签: python haskell functional-programming elixir

我遇到了Mark Dominus' blog post,它描述了使用函数式编程技术(特别是Monads)在Python中解决"SEND+MORE=MONEY" puzzle

这里的谜题总结了死链接:

    S E N D   | Find each character's *unique* numerical value, such that
+   M O R E   | the addition on the left is valid. There are no leading zeros.
-----------
= M O N E Y

我一直在寻找机会学习一些纯函数编程,特别是Elixir,这看起来非常合适。

我可以在Elixir中实现美国相似版本的Mark Dominus的Python代码:

defmodule Smm do
  def make_set(ls), do: Enum.into(ls, HashSet.new)

  def to_number([]), do: :error
  def to_number(ls), do: Enum.join(ls) |> Integer.parse |> elem(0)

  def remove(hs, ls), do: Set.difference(hs, Enum.into(ls, HashSet.new))

  def let(x, func), do: func.(x)

  def guard(predicate, func) when predicate, do: func.()
  def guard(predicate, func), do: []
end

digits = Smm.make_set(0..9)

Enum.map( Smm.remove(digits, [0]), fn s ->
Enum.map( Smm.remove(digits, [s]), fn e ->
Enum.map( Smm.remove(digits, [s,e]), fn n ->
Enum.map( Smm.remove(digits, [s,e,n]), fn d ->
Smm.let(Smm.to_number([s,e,n,d]), fn w_send ->
Enum.map( Smm.remove(digits, [0,s,e,n,d]), fn m ->
Enum.map( Smm.remove(digits, [s,e,n,d,m]), fn o ->
Enum.map( Smm.remove(digits, [s,e,n,d,m,o]), fn r ->
Smm.let(Smm.to_number([m,o,r,e]), fn w_more ->
Enum.map( Smm.remove(digits, [s,e,n,d,m,o,r]), fn y ->
Smm.let(Smm.to_number([m,o,n,e,y]), fn w_money ->
Smm.guard(w_send + w_more == w_money, fn ->
[w_send, w_more, w_money] |> Enum.map( &(IO.puts(&1)) )
end)end)end)end)end)end)end)end)end)end)end)end)        # (╯°□°)╯︵ ┻━┻

但有些东西告诉我必须绕过疯狂嵌套的匿名函数和随后的表格翻转;这就是为什么存在纯函数式语言的原因吧?

看着Mark Dominus'previous blog post in which he solves the puzzle with Haskell,我看到他正在使用含糖版本的Haskell的“绑定”运算符>>=来消除表格翻转的冲动......但我没有{{3所以我对这篇博文中提供的代码没有很强的把握。

我很确定我在Elixir实现中缺少的是使用管道运算符|>,这实际上对我来说是一个很大的吸引力(我非常熟悉Unix管道) 。我尝试过使用管道和Enum.{map,reduce}的许多种类,但我总是回到第一个方向。

有人可以提供任何建议吗?理想情况下,我正在为Elixir中的这个难题寻找更具惯用性的函数式编程解决方案。

2 个答案:

答案 0 :(得分:3)

您可以在此处查看:What is the "|>" symbol's purpose in Elixir?,了解|>运算符的概述。但基本的想法是a |> f(b, c)f(a, b, c)相同。当您执行a |> f(b) |> g(c)之类的操作时,这非常有用,上面的规则与g(f(a, b), c)相同,但读取得更好。

据说,|>运算符(称为管道)不是monadic绑定(>>=),并且不会让你“变平”#34;像>>=这样的深层嵌套循环。对于在Elixir中看起来更好的任务的替代方法,您可以:

  1. 使用实现Monads语法的库(或添加您自己的语法),如MonadEx
  2. 停止使用这种循环方法,例如使用递归函数预先生成数字到字母的赋值,如下所示:

    defmodule Smm do
      # some more things
    
      def assignments(0, _), do: [[]]
      def assignments(n, digits \\ Enum.into(0..9, HashSet.new)) do
        digits
        |> Stream.flat_map(fn (d) ->
          for rest <- assignments(n - 1, Set.delete(digits, d)) do
            [d | rest]
          end
        end)
      end
    end
    
    for [s, e, n, d, m, o, r, y] <- Smm.assignments(8) do
      w_send = Smm.to_number([s, e, n, d])
      w_more = Smm.to_number([m, o, r, e])
      w_money = Smm.to_number([m, o, n, e, y])
    
      if s > 0 && m > 0 && (w_send + w_more == w_money) do
        IO.inspect([w_send, w_more, w_money])
      end
    end
    

答案 1 :(得分:3)

这种语法分心在Haskell中被消除了两件事:运算符关联性和lambdas的一个很好的语言规则。

associativity rules消除语法中不必要的括号。例如,在Elixir中,添加关联到左侧,因此当您编写a + 2 + x时,它将被解释为(a + 2) + x。关联性规则可以让你摆脱括号。如果您的意思是a + (2 + x),则必须明确地写出来。

你可以获得操作员关联来帮助Elixir,有些人已经有了。 MonadEx库定义了一个绑定操作符~>>,可以让你编写程序的内容大致为

    Smm.remove(digits, [0])
    ~>> fn s -> Smm.remove(digits, [s])
    ~>> fn e -> Smm.remove(digits, [s,e])
    ~>> fn n -> Smm.remove(digits, [s,e,n])
    ~>> fn d -> return(Smm.to_number([s,e,n,d]))
    ~>> fn w_send -> Smm.remove(digits, [0,s,e,n,d])
    ~>> fn m -> Smm.remove(digits, [s,e,n,d,m])
    ~>> fn o -> Smm.remove(digits, [s,e,n,d,m,o])
    ~>> fn r -> return(Smm.to_number([m,o,r,e]))
    ~>> fn w_more -> Smm.remove(digits, [s,e,n,d,m,o,r])
    ~>> fn y -> return(Smm.to_number([m,o,n,e,y]))
    ~>> fn w_money -> Smm.guard(w_send + w_more == w_money)
    ~>> fn -> return([w_send, w_more, w_money])
    end end end end end end end end end end end end

运算符关联性并没有消除所有以相同位置结尾的lambda表达式。后面的表达式需要在早期的lambdas中,以便他们可以看到前面介绍的变量。 Haskell通过简单的语法规则"lambda abstractions ... extend as far to the right as possible"摆脱了这种分心。因为lambdas一直延伸到右边,所以用相同样式编写的Haskell代码没有一大堆末端括号。

solutions = remove [0] digits >>= \s ->
            remove [s] digits >>= \e ->
            remove [s,e] digits >>= \n ->
            remove [s,e,n] digits >>= \d ->
            let send = to_number [s,e,n,d]
            in remove [0,s,e,n,d] digits >>= \m ->
            remove [s,e,n,d,m] digits >>= \o ->
            remove [s,e,n,d,m,o] digits >>= \r ->
            let more = to_number [m,o,r,e]
            in remove [s,e,n,d,m,o,r] digits >>= \y ->
            let money = to_number [m,o,n,e,y] in
            guard (send + more == money) >>= \_ ->
            return (send, more, money)

我无法想象Elixir的相应技巧。每个fn最后都必须end,因此会有与绑定一样多的end个。我想你只需要继续翻转表格(╯°□°)╯︵ ┻━┻