Erlang/Elixir Ruby 模仿:|>

chenge · August 04, 2017 · Last by steve-chan-clojure replied at August 14, 2017 · 7117 hits
s = "a b c d"
val = s.
  (~:split, " ").
  (~:join, "-").
   ~:capitalize

val
# => "A-b-c-d"

https://6ftdan.com/allyourdev/2017/08/03/elixir-envy-%e1%90%85-ruby/

s.split(" ").join("-").capitalize

没理解为啥要模仿 -> 这种操作符。

Reply to yfractal

我想是引入函数思维吧,总是生成新的对象,程序更稳健一些。

Reply to chenge

一楼不是吗?

在 Ruby 里用这个意义不大啊。。

Reply to Rei

也是,不过没这么明显吧。ruby 里有些方法是改变对象自身的。

-> 的作用是较少函数式中链式调用的嵌套,elixir, clojure 都是这样,ruby 是面向对象的,不存在这个问题

Reply to Rei

不是。Enumerable#map(func) 和 map(func, Enumerable) 是完全不同的风格。

Reply to darkbaby123

Breaker.break_workds 就行了吧?

Breaker.break_words(some_text).map {|word| word[:text] }

为了 OO 而 OO 就会连 print object 都得写成 object.print 了...

Ruby 里要不要 patch String, 得看这个方法是否真的属于 String, 怕泄漏只要局部 patch 你还可以用 using, 但是感觉这个 using 也没什么人用,我们要的就是全局污染的爽快...

链起来可以省掉起变量名的麻烦,但是链长了也不好 debug ... pipe 比链是更强一点,加上一些组合子,不管怎么都能 pipe 上 (就和 jQuery 的链式调用一样了...), 如果能再简洁点我就入教了

但一步一个脚印的指令式代码其实有时还挺适合 debug 的

所以我才在后面接了一句,如果过程中再加一层抹开调用会如何?我暂时也想不到一个现实中的例子,但假如有两层模块调用大概是这样:

# 一般都会写成这样比较易读和 debug
s1 = ServiceOne.new(some_data)
r1 = s1.do_something
s2 = ServiceTwo.new(r1)
r2 = s2.do_something_else

# 写成一行就这样了
ServiceTwo.new(ServiceOne.new(some_data).do_something).do_something_else

这种情况下老实写过程式代码是更好的选择。但在 Elixir 中 有时候 还能用一下 pipe 并且不丧失可读性:

some_data
|> ServiceOne.do_something
|> ServiceTwo.do_something_else

我觉得根本上还是语言特性对职责划分的影响。OOP 更建议把职责跟某个相应的 object 绑在一起,所以方法调用会有点局限。这样的好处是因为 object 自带上下文所以通常可以少写一些代码。而 FP(或者说 Elixir,我对其他 FP 语言也不了解)因为一开始就分离了数据和函数,所以组合的时候得严格的写清楚。这是上面的例子中 Ruby 代码通常比 Elixir 代码简短得多的根本原因。

至于打 patch 则比较矛盾。Ruby 中的对象方法有一部分是遵循职责划分,但也有不少纯粹是一层代理。就是为了写的更爽(让程序员更开心)。以 OOP 的思想来看,很难说 10.times { ... }"some date".to_time 真是按职责划分来的。不过大部分人喜欢用就是硬道理。Refinement 这个技术没推广开来也是有原因的,RubyConf 2015 上这个演讲 总结得很到位。大体上说就是到处 using 太麻烦,Rails 作为魔法界标杆都不用,还有 OOP 思想的回归为一些问题提供了另一种解决方法。本来喜欢魔法的用火球,不喜欢的觉得太危险就投奔科学去了。Refinement 告诉你为了安全扔火球之前先背一遍 xxx ……

顺带一提,在 pipe 过程中 debug 太方便了,当然 Ruby 也可以 obj.tap(&:inspect)

some_data
|> step1
|> IO.inspect
|> step2
Reply to darkbaby123

其实 ruby 完全可以定义一些运算符,甚至超算符...

some_data
>> ServiceOne.do_something
>> ServiceOne.do_something_else

用这套配组合子可以玩的很开心……

但是要 ruby way 的话,我想应该是这样子:

some_data.pipe do
  call ServiceOne.new(_).do_something
  call ServiceTwo.new(_).do_something_else
end

因为管道本质上是要把占位符拿出来,这样也许不算太优雅,但是也许有些地方能用到,另外个人觉得 binding.pry 其实比 puts 要方便.....

抄送 @luikore ;-)

@mizuhashi 不太了解超算符是什么,不过看上去是挺有意思。

管道本质上并不是把占位符拿出来(我也不太明天你的意思),而是跟 Unix pipe 一样把上一个步骤的输出传递到下一个步骤的输入。让数据转换流程的写法更方便一点。

你的第二个例子里 call ServiceOne.new(_).do_something 应该是无效的,只能是类似 call ServiceOne, :do_something 这样。问题在于 do_something 的结果是计算好之后再传给 call ,无法做到把 some_data 或者上一个步骤的值传给 service 再运算。

Reply to darkbaby123

可以的,_就是上一行的结果,call的作用就是赋值给_https://ruby-china.org/topics/31848

Reply to darkbaby123

想玩魔法的话其实 call 也不用的,也不用 method_missing,在执行 block 之前做个宏替换:

require 'ruby_parser'
require 'ruby2ruby'
require 'method_source'

class Object
  class Proxy < BasicObject
    def call value
      @_ = value
    end

    def _
      @_
    end

    def initialize target
      @_ = target
    end
  end

  def pipe &block
    source = block.source
    block_body = RubyParser.new.process(source).to_a[3]
    head, *sequences = block_body
    sequences.map!{|x| [:call, nil, :call, x]}
    ast = Sexp.from_array([:block, *sequences])
    Proxy.new(self).instance_eval Ruby2Ruby.new.process(ast)
  end
end

#==========业务代码分割线=========

class ServiceA
  def initialize(n); @n = n; end

  def do_something
    p @n
    @n + 1
  end
end

class ServiceB
  def initialize(n); @n = n; end

  def do_something_else
    p @n
  end
end

some_data = 1
some_data.pipe do
  ServiceA.new(_).do_something  # => print 1
  ServiceB.new(_).do_something_else  # => print 2
end

来个 Clojure 版本,->

(-> "a b c d"
    (str/split #" ")
    (#(str/join "-" %))
    str/capitalize)

;;=> "A-b-c-d"

Clojure -> 宏的源码太简单了,@mizuhashi

(defmacro ->
  [x & forms]
  (loop [x x, forms forms]
    (if forms
      (let [form (first forms)
            threaded (if (seq? form)
                       (with-meta `(~(first form) ~x ~@(next form)) (meta form))
                       (list form x))]
        (recur threaded (next forms)))
      x)))

Lisp 强大的宏能力 😍 😍 😍

你的宏代码很难看懂啊。可能对你来说简单吧。

Reply to chenge

不难看懂的,因为 Lisp 语言都会用 macroexpand 展开来看 代码是如何被 "编辑"了的 , 宏做了什么列表 (代码和数据,一切都是列表) 操作,如下面的两个例子:

(-> 100 (+ 1) (+ 2) (+ 3)) ;; => 106

(macroexpand-1 '(-> 100 (+ 1) (+ 2) (+ 3)))
;; => (+ (+ (+ 100 1) 2) 3)  ;; 代码展开的样子
(defmacro unless [expr form]
  (list `if expr nil form))

(macroexpand-1 '(unless false "Hi, Clojure!"))
;; => (if false nil "Hi, Clojure!") ;; 代码展开的样子

最重要的是,Clojure 的宏具有强大的杀伤力,却不会影响性能,因为宏的编译时和运行时是分开的。在运行时前,代码已经编译好了。

学了 Clojure 和 Emacs Lisp 之后,我才知道 Matz 为什么这么喜欢 Lisp 了 😍 😍 😍

Elisp 改变了 Matz 的人生。😝

元编程的最高境界就是 Lisp 了 😍

You need to Sign in before reply, if you don't have an account, please Sign up first.