• @tenderlove: Time to learn Elixir! at 2017年10月18日

    用了都大半年了

  • Ruby、函数式和 RESTful at 2017年09月30日

    没太懂 API 幂等的含义,POST 不幂等是因为创建资源是个从无到有的过程?还是因为请求参数中可以不带某些 default value ?不过这篇 slide 确实不错。

  • [Elixir 基础] 复合类型: List at 2017年09月18日

    List 是单链表结构,每次添加到尾部都会完全遍历一遍。如果有这种操作(写某些算法经常出现),建议添加到 List 头部最后 Enum.reverse ,这样一次遍历效率高很多

  • 没必要。文件命名都是用小写和 dash 的格式,混几个大写自己看得也费劲。带有随机生成的 code 的文件名是唯一可能碰到坑的问题。不过本地环境又不会生成那么多……

  • 玩 Elixir 吧,语法对 Ruby 程序员来说友好很多

  • 解析邮件碰到的那些坑 at 2017年08月21日

    charlock_holmes 这名字起得好,哈哈

  • Ruby 模仿:|> at 2017年08月12日

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

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

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

  • 部分赞同。Rails 兴起时的前端还是比较简单的,甚至没有专门的前端划分(往往叫 “套” 页面的)。那时后端工程师兼任下前端的工程部分是理所当然的。现在前端越来越细化,已经形成一个单独的分类了。想做得好也要投入很多的时间和精力,从社会分工和专精的角度来说全栈已经不是太必要了。如果想深入发展的话,找一个主要方向专攻,适当放弃一些东西才能做的更好。

    但社会各个行业都需要一些万精油的,各方面都会一些也不代表没竞争力。最终还是看各人定位和找坑的眼光……

    而且话说回来,后端这个词比某个语言的范围更广泛得多(前端其实也一样)。

  • Ruby 模仿:|> at 2017年08月06日

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

    # 一般都会写成这样比较易读和 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
    
  • Ruby 模仿:|> at 2017年08月04日

    虽然我更喜欢 Elixir 的 pipe ,但它强行搬到 Ruby 里来确实没什么意义,而且还是用这种特殊三角字符…… 而且这种项目其实不是在借鉴其他语言的功能,而是证明原语言的强大(看我还能做到这个哦)。类似的还有 Elixir 的 OOP ,这玩意更强大,连继承都实现了。

    从语法角度来说,pipe (|> 不是 ->) 的用途是让函数的嵌套调用变得更优美,和 OO 的链式调用不是一回事。但如果从 用类似链条的形式来表示数据转换 的角度看,两者目的是相同的。这种情况下我觉得两者各有优劣。

    比如楼主的例子写成 Elixir 应该是:

    str = "..."
    # pipe
    str |> String.split(" ") |> Enum.join("-") |> String.capitalize()
    # non pipe
    String.capitalize(Enum.join(String.split(str, " "), "-"))
    

    而 Ruby :

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

    Ruby 的写法很容易让人心生好感,代码比 Elixir 短得多。实际上 大部分时候 Elixir 的代码都比 Ruby 要长,甚至比不用 pipe 的代码还长一点 。那 Elixir 这样做的好处是什么呢?

    一个好处是更 Explicit 。从 Elixir 代码中你能很直观的看到每个环节的数据大概是什么类型,每个函数出自哪里,从而间接的少犯些错误。Eplicit 的思路贯穿 Elixir 语言和生态,pipe 只是其中一个缩影。是否要 Eplicit 则是见仁见智。我只能说个人更喜欢这种风格,上面的例子太简单看不出来优势,但在复杂的流程中把代码表述得更严格是有好处的。

    另一个好处是 更好的职责划分和多态 。我们经常会碰到一个 object (或者一份数据)在不同的场景下需要不同的行为。这种情况下以 OO 的方式大概有以下几种做法:

    1. 把一个 object 变得功能超级多(比如 include 多个模块)。
    2. 按不同的职责建立多个 wrapper object ,封装思想的一类应用。
    3. 还是建立多个功能模块,在运行时动态跟主体 object 组合。

    现实情况下 1 和 2 比较常见。但有各自的缺点。1 会让 object 方法过多,混入的模块不受控制容易方法重名导致覆盖。2 中 wrapper 和 inner object 毕竟不是一个 context(我指的 self),访问一些内部变量时会有点不方便。3 没有前两者的问题,但在 Ruby 中通常是以 Object#extend 来实现,因此有明显的性能问题。有一个模式 DCI 用的是这种技巧。引起注意不到一年后就没什么关注了,可能也跟性能原因有关。其实我觉得这个模式倒很适合 JavaScript 。

    相对而言 FP 把数据和行为(函数)分离的方式能更容易的应对这个问题。一个数据可以经过不同模块的函数转换后得到一个结果,这个过程用 pipe 表达很清晰自然。

    举个例子,以下的 Elixir 代码把一段文本分词后形成一个列表,假设 Breaker 是分词的模块:

    some_text
    |> Breaker.break_words    # => [%{text: "word one", type: "noun"}, %{text: "word two", type: "verb"}]
    |> Enum.map(&Map.get(&1, :text))    # => ["word one", "word two"]
    

    如果用 Ruby 来实现,可能会这样:

    # 注入一些方法到 String 里去
    some_text.break_words.map { |word| word[:text] }
    some_text.to_breaker.break_words.map { |word| word[:text] }
    
    # 现在更推荐 OO 的写法,不过没链式调用了。
    breaker = Breaker.new(some_text)
    breaker.break_words.map { ... }
    

    第一个方法需要污染 String ,第二个方法结构更好一些,其实省去 breaker 这个变量后写一行还是没问题的,但 如果流程中再加入一个模块调用呢? 这种情况下 pipe 可能还能用,但链式调用是没戏了。

    再看另一个例子,对一组数据的转换:

    (1..1_000_000)
    |> Enum.map(&do_something/1)   # 一种简写,代替 map(fn i -> do_something(i) end)
    |> Enum.reduce(&merge_something/2)
    |> OtherMod.save_to_db
    

    如果觉得数据很大,每次转换步骤都会生成一个很大的列表,可以换成 Stream

    (1..1_000_000)
    |> Stream.map(&do_something/1)
    |> Enum.reduce(&merge_something/2)
    |> OtherMod.save_to_db
    

    如果需要进一步增加效率,可以用 Flow 分多个进程去同时处理:

    (1..1_000_000)
    |> Flow.from_enumerable
    |> Flow.map(&do_something/1)
    |> Enum.reduce(&merge_something/2)
    |> OtherMod.save_to_db
    

    这个过程中参与的模块越来越多,但并没有因为模块加多而丧失表达力。而且就算把整个流程拆分到多个函数中去也能大概猜到每次转换的数据是什么。Ruby 则或多或少要受限于 self 能够响应哪些方法才能继续链式。强行链式就得往 self 里加方法来形成链条。

    最后说一句,pipe 和链式调用一样都有其限制,不是所有地方都能用 pipe 表达,这时也不必强行修改函数签名让它能够 pipe 下去。