• [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 下去。

  • macOS 只能用 Ctrl-Shift-6 ,原因见 这里

    我是用键绑定解决的 nnoremap <leader>bp <C-^>

  • erlang 与状态机 at 2017年07月20日

    这两个模块是有文档链接的,LZ 可以更新一下文章:

  • 别解决 ignore 的问题,解决同事……

  • 各种制定标准,稳固统治地位

  • @chenge 忘了参数个数能不能检查了。RSpec 这个特性我就用了一段时间,后续代码都没用它。除了觉得用处有限之外,也觉得这种方式不太符合 duck typing 的本质 -- 本身定义 mock object 的行为就够了,为什么还要耦合一个类型?

    Elixir 也是动态类型语言,typespec 需要依靠 Dialyzer 去做检查。

  • @chenge 这跟测试写没写全不是一个问题。假设 A depends on B 。你对 A 做测试,把依赖 B 用 mock 替换掉了。当然 B 也有自己的测试。一个月后你改了 B 的接口。这个时候 A 的测试显示一切正常。但程序已经无法运行了。

    Java, C# 等静态语言里有严格的接口依赖,碰到这种情况还会报个编译错误。Ruby 吭都不会吭一声。当然 RSpec 有一些手段可以部分预防这一点,比如 instance_doubleclass_double 只允许你 stub 已经实例/类中已经定义了的方法,不过仅此而已了,检测不到方法参数和返回值,这两个方法提供的保障就是聊胜于无。

    接口改了测试当然也要改。问题是到时候还记不记得这件事?比如你是否能够记得每次改 B 的时候都去搜一下它到底被哪些模块依赖了?最后你会发现要保障模块协同工作,还是得靠集成测试。

  • 我一般用 mock 的情况:

    1. 某个内部实现复杂和关联特别多的依赖,为了加快测试速度
    2. 不是自己写的部分(第三方服务),为了提供稳定的前置条件
    3. 跟别人约定好接口但还没实现的模块,为了开发解耦

    其实第一点也是为了提供稳定的前置条件。相比构造 8 个 model 数据才能写点测试,一个 mock 显然更低成本(偷懒)。大量的 mock 看似解耦了,但代价是没法测到多个模块协作的情况。对 Ruby 这种 duck typing 的语言来说更是灾难。有时候模块接口都改了但测试不会报错(因为依赖全是 mock)。最后往往测试全绿但项目都跑不起来。

    我觉得好的测试的标准是 “改动了软件行为测试就该挂掉” ,速度方面能够接受就行。

  • @FrankFang 原型链和祖先链确实有非常大的相似性。两者可以对比着学习。之前我用 JS 的 decorator 模拟过 Ruby 的 mixin 。不过实现 include 容易, prepend 就麻烦点。

  • class 早在 ES2015 时就有了,Node 估计支持得稍晚一点。而且 class 只是语法糖,背后机制还是基于原型链的。它的用处是提供了一个标准的类型声明和继承的写法,以后就不用造各种非官方的轮子了。

    JS 相比 Ruby 更好的一个语法特性是 destructuring (解构) ,相当于弱化版的 pattern matching ,其他的貌似没啥印象深刻的。

  • 为什么相关话题是 男人在结婚前都是忍着脾气,然后结婚后就爆发出来了么! …… 这个相关度计算不错……

  • Rails 已是业内性能标杆,基本每个 web 框架都会拿它来比比

  • @jasl 你如果写 gem 的话,完全可以定义一个 serializer API ,其他的让别人折腾去 😂

  • 问卷其实跟自定义表单有些差别。自定义表单更类似标准的 form ,区别只是每一个 item 的 key 是一个动态 id (对应静态表单中中每一项的 name),然后 value 可以多种不同的类型。具体做法取决于实现。但问卷就不那么标准。比如:

    1. 一个选择题加若干个其他选项,这几乎等于一个选择题加多个填空题。
    2. 有些异形题(比如矩阵)是二维的,需要两个 key 来关联答案。
    3. 循环的加入会导致下面的子题目扩展成多份,这时候很难用一个 model 的 id 作为 item key 。目前的实现是用多个不同属性组合来关联答案的。

    目前系统的做法其实挺简单,就是提交一个数组,每一项是答案。每个答案有各自不同的关联属性。举个例子:

    [
      {NodeID: "{uuid}", OptionRID: "{uuid}", OptionCID: "{uuid or null}", OtherKeys: "..."},
      {...}
    ]
    

    我现在在重构 (xie) 后端系统,也在思考更好的做法,不过要兼顾现在所有的功能实在很麻烦……

  • @jasl 嗯,是的。一个明确的结构更容易维护。分别建表完全可行,我这样就是偷懒 😄

  • @jasl 嗯,我是为额外属性建立 Ecto schema 嵌套在主 schema 下面,方便 app struct 和 database row 的互相转换。大概这样:

    defmodule Question do
      schema "questions" do
        field :type,   TypeEnum
        # 存放额外属性,Elixir 里为 map ,对应 Ruby 的 hash ,PostgreSQL 里面为 jsonb
        field :config, :map
      end
    
      # 每题定义一个子类型,用来格式化 config 的数据
      defmodule Select do
        embedded_schema do
          field :multi_sel, :boolean
          field :min_sel,   :integer
          field :max_sel,   :integer
        end
      end
    
      defmodule ValueMark do
        # ...
      end
    
      # 根据 type 选择题型相关的 schema 来转换数据
      def cast_config(ques, params) do
        mod = get_config_module(params[:type]) # get Select module
        config = params["config"] |> transform_to_schema(mod) |> transform_back_to_map_if_valid
        put_change(ques, :config, config)
      end
    end
    

    用 jsonb 有两个原因:

    • 每个题型子类型的属性太多了,如果少的话也可以用类似 STI 那样的做法,建立一堆额外的数据库字段。
    • 受制于 Ecto ,没办法动态扩展 field ,只能用嵌套的形式了。不过我最近碰到了不同的子类型有公用属性的情况(比如所有选择类的题目都有多选和随机),打算对子类型用动态的 schema 重构一下。

    这种方法的主要缺陷在于没法保证引用完整性,比如额外属性里有图片资源的 id ,我也在考虑要不要特事特办,额外开字段存储部分数据。

    问题间跳转就一言难尽了,前端流程处理可以问问 @nightire ,后端这边我们是存储了每个题对应的下一题的路径数据,因为也有额外属性不定的问题,所以也是 jsonb 存的。

  • @jasl 我现在在巧思负责新后端的开发。但我的场景不算是 form 的场景,只是同一个大类型的不同 object 都会有一些额外的属性,而且这些额外属性是可预测的(除了需求)而不是用户自定义的。目前是在应用层面用 hash/map 表示并存进数据库的 jsonb 字段里。

    我们的问卷系统倒是跟自定义 form 有少数重叠部分,不过因为题目的渲染和验证等逻辑都是在前端实现的,后端更多的是持久化 “元数据” ,所以也不存在用 Elixir 动态渲染页面的情况。

  • @jasl 👍 ,期待你开源相关的存储方案