我是怎样学习新编程语言的

Elixir

说服自己

学习新的编程语言的最终目的是解决实际问题。掌握编程语言的过程,在某种程度上近似学习一种新的工程实践。不仅解决问题固然可乐,学习的过程也同样充满了新鲜感,不过需要谨防的是新鲜感带来的胜任力错觉。

胜任力错觉指的是反复接触新东西,发现不用花费什么气力就理解了其中所有的内容。说的简单点,就是自以为是。这种胜任力错觉导致最常见的后果是以为掌握了某种技能,真正开始解决问题时,要么是半天摸不着头绪,要么就是处处掣肘。所以我始终相信,阅读是一码事,理解是一码事,掌握还是另一码事,所谓一码归一码,大抵就是这么回事。

以终为始,方得始终。老子(真·老子,非我)也说,慎终如始,则无败事。这里的“终”就是目标,在软件工程中,有一种实践很好得反映了这种做事方式——测试驱动开发。借我司的一位牛人的原话:看一个人会不会测试驱动开发,不是看他的测试写得好不好,而是要看他是不是始终从测试出发去解决问题。脑子里条件反射的就是测试该怎么测?这种才是测试驱动开发的实质。

学习,说白了就是一个不会到会的过程,这里头最难的是学会了什么?在学习方法上,我们很多时候喜欢遵循前人的套路,美其名曰知识体系化。我承认体系是前人经验和群体智慧的积累,但是学习体系不代表你具备形成体系的能力,就像你学习了著名开发框架(Spring or Rails)也不会说你能开发这套框架一样。学习的关键还是发散、收敛和再发散、再收敛的渐进过程,感性的定性分析到理性的定量分析,在不断丰富和修正认知,处处用实践检验认知。这种过程坚持下来,得到就不单单是知识,可能是元知识(方法论)或者智慧。

看书抄代码是个学习的好方法,不过书中的例子一般都被加工(简化)过,我们很容易陷入套路中,谨记胜任力陷阱。比较推荐的方式,自己认准一段有用的程序,反复练习(也可以每次增加些体系化的功能)直到娴熟。在接触新语言时,不去看一套完整的语言体系,而是事先把这段程序可能用到的基本类型、数据结构、流程控制结构、模块化和功能组件列出来,然后去找它们在这门语言中对应的实现。

有目的地试错

我常用的练手程序叫tree,功能是list contents of directories in a tree-like format. 这个程序需要用到的基本构件有:

基本类型(basic type)
1. str
 
数据结构(data structure)
1. list
2. map

流程控制结构(control flow structure)
1. if, else
2. recursion

模块化(modulize)
1. function
2. module/namspace/package

功能组件(function components)
1. IO
2. File
3. Path

分类清晰之后,对应找起来很方便,有的基本不用找,经验足矣。现在的编程语言基本都有repl,多尝试几遍就有了感性认识。我说的很轻松,但是如果不去尝试,一样会难住。Elixir中有iex命令作为repl,而且这门语言深受Clojure的影响,尤其是文档和例子方面很充足,对于初学者再友好不过。

换种思维

在编写tree的过程中,我会时不时停下来思考Elixir在某个功能点上应该怎么用才好?因为历史上,把Java的代码写成C风格的人不在少数,这足以让人警惕。再说,学会用新语言的思维方式编程是我初始的目的之一。

这里举个例子,map的key使用哪种基本类型会比较合适?Clojure中有keyword,如{:name "clojure"},而Python中并没有这样的数据类型,我只好使用{'name': "python"},那么Elixir呢?它推荐的是atom/symbol,%{:name => "elixir"} #or %{name: "elixir"}

遇到需要join path的时候,凭借原来的经验,我会去寻找Path模块。具体可以去问谷歌,也可以问repl

iex<1> h Path.join
or
iex<1> Path.join <TAB> #用tab键
join/1    join/2

看到join/1 join/2的时候,我有些许迷茫,但是很快就变成了欣喜。我们知道,在动态类型语言中,arity指的是方法参数的个数,这里的1和2其实表明的就是join有两个重载的方法,分别接受一个参数和两个参数。更进一步,arity是方法(函数)实现静态多态的依据之一。再进一步,多态是函数的特性,而非OO中固化下来的概念——类的特性。

组织代码

上面的验证只需要repl就足够了。但是,真正编写还是得有组织和结构。软件工程中,控制复杂度(复杂度从来不会被消除)的基本法则就是模块化。这就引出了module和function,还有对模块可见性(private, public etc.)的修饰。

defmodule Tree do
  defp tree_format(parent_dir, dir_name) do
    %{:name => dir_name, :children => []}
  end
end

defp定义了一个私有的方法tree_format,它是用来格式化目录的。目录结构是树形结构,所以很容易递归实现。

  defp children(path) do
    if (path |> File.dir?) do
      File.ls!(path) |> Enum.map(fn f -> tree_format(path, f) end)
    else
      []
    end
  end

  defp tree_format(parent_dir \\ ".", dir_name) do
    %{:name => dir_name, :children => Path.join(parent_dir, dir_name) |> children}
  end

在利用递归的过程中,我使用File.ls!(查文档,注意!号)列出子目录,然后递归地格式化。这些都比较好理解,不过这里其实出现了两个新的玩意(当然也不是一蹴而就的,认识之后才重构成这样)。一个是\\ ".",还有一个是|>。第一个比较容易猜,叫做默认参数(default arguments);第二个有Clojure基础的也手到擒来,叫做管道操作符(pipe operator),用来将左边表达式的结果传入右边方法的首个参数。这里就是children(path)path.

结构,解构

完成目录结构的格式化,接下来需要做的是渲染这组树状的数据。

  defp decorate(is_last?, [parent | children]) do
    prefix_first = (if (is_last?), do: "└── ", else: "├── ")
    prefix_rest = (if (is_last?), do: "    ", else: "│   ")
    [prefix_first <> parent | children |> Enum.map(fn child -> prefix_rest <> child end)]
  end

  defp render_tree(%{name: dir_name, children: children}) do
    [dir_name 
     | children 
     |> Enum.with_index(1)
     |> Enum.map(fn {child, index} -> decorate(length(children) == index, render_tree(child)) end) 
     |> Enum.flat_map(fn x -> x end)]
  end

到这里,我学到的是参数解构(arguments destructing),map-indexed的新实现,字符串的拼接(string concatenation)还有列表元素的前置操作。

Elixir和所有函数式编程语言一样,具备强大的模式匹配(Pattern matching)的功能,参数解构其实就是其中的一个应用场景。

%{name: dir_name, children: children}
matching
%{:name => ".", :children => ["tree.exs"]}
# ->
dir_name == "."
children == ["tree.exs"]

渲染的过程也是递归的。最终返回的是一个加上分支标识前缀的列表

[dir_name | children]

这是一种将dir_name前置到children列表头部,形成新列表的做法。和Clojure(绝大数Lisp)中的(cons dir_name children)类似。

操作符|除了可以前置列表元素,递归解构也是一把好手。

  defp decorate(is_last?, [parent | children]) do
    ...
  end

参数列表中的[parent | children],解构出了列表的head和rest,这对于递归简直就是福音。

在添加前缀的步骤[prefix_first <> parent...]中,经验里字符串的拼接常用符号+不起作用了,换成了<>,这个是靠试错得出来的。

除了说到的这部分内容,我还运用了Enum.map, Enum.with_index, Enum.flat_map等函数式语言的标配。这些零散的知识点,可以添加到基本构件中,以便持续改进。

入口

程序要执行,就需要一个入口。每次我都会猜猜argv会在哪里出现呢?是sys(Python),os(Go),还是process(Node.js),这回又猜错了,Elixir管这个叫做System.

  def main([dir | _]) do
    dir |> tree_format |> render_tree |> Enum.join("\n") |> IO.puts
  end
# ---
Tree.main(System.argv)
# ---
$ elixir tree.exs .

重构

这里重构的目的是让程序更加贴近Elixir的表达习惯,那么哪里不是很符合Elixir风格呢?我注意到了if...else,可以考虑模式匹配实现多态。

  defp children(path) do
    if (path |> File.dir?) do
      File.ls!(path) |> Enum.map(fn f -> tree_format(path, f) end)
    else
      []
    end
  end

File.ls!中的!表示如果指定目录有问题,函数会抛出error或者异常。然而,Elixir还给出了一个File.ls方法,即便出错,也不会有抛出的动作,而是返回{:error, ...}的元组,至于正常结果,则是{:ok, ...}. 这恰恰可以使用模式匹配做动态分派了。

  defp children(parent) do
    children(parent |> File.ls, parent)
  end

  defp children({:error, _}, parent) do
    []
  end

  defp children({:ok, sub_dir}, parent) do
      sub_dir |> Enum.map(fn child -> tree_format(parent, child) end)
  end

一旦children(parent |> File.ls, parent)中的parent不是目录,File.ls返回的就会是{:error, ...}元组,它会被分派到对应的方法上,这里直接返回一个空的列表。反之,我们就可以拿到解构之后的子目录sub_dir进行交互递归,实现全部子目录的格式化。

小结

在学习Elixir的过程中我收获了很多乐趣,不过,这离掌握Elixir还有很远的距离。我曾经看过一部科幻电影“降临”,剧情受到了萨丕尔-沃夫假说(语言相对性原理)的影响,这个假说提到:人类的思考模式受到其使用语言的影响,因而对同一事物时可能会有不同的看法。既然如此,那么自然语言也好,编程语言也罢,如果能换种思维方式解决同一种问题,说不定能收获些奇奇怪怪的东西,编程之路,道阻且长,开心就好。 -- 2018-06-08


如何高效地学习编程语言 怎样才算学会Python Elixir 萨丕尔-沃夫假说

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏前端说吧

JS-过滤敏感词【RegExp】

5206
来自专栏数据结构与算法

HDU 2089 不要62

不要62 Problem Description 杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个...

28710
来自专栏IT大咖说

前端专家聊JS语言家族新成员——R&B

摘要 相信大家对以CoffeeScript、TypeScript为代表的编译到JavaScript的语言已经不陌生。本次分享将介绍 JS 平台语言家族的重要新成...

4018
来自专栏Alan's Lab

JS 变量提升

问到 JS 一些细节问题的时候发挥比较糟糕,有些是知道反应得太慢,有些是压根没接触过,还是积累的太少了。这篇的 JS 变量提升问题就是从没有接触过的,网上一搜一...

2372
来自专栏信数据得永生

JavaScript 编程精解 中文第三版 七、项目:机器人

3346
来自专栏逸鹏说道

重温数据结构系列随笔:数据结构的基本概念

现在项目已经踏上正轨,有不少时间可以用来学习,昨晚发现柜子里那本大学时候啃过无数遍的(数据结构 C语言版),那真的无限感叹啊,初恋女友啊,大学回忆啊都涌上心头。...

2884
来自专栏牛客网

滴滴java新锐实习一面

1720
来自专栏有趣的Python

1-玩转数据结构-欢迎学习数据结构

对于数据的存储这个任务,解决方案是很多的。我们需要根据应用的不同,灵活选择最合适的数据结构

2.1K5
来自专栏静默虚空的博客

[算法题] 安排会议室——贪心算法的应用

题目描述 [题目描述] 在大公司里,会议是很多的,开会得有场子,要场子你得先在电子流里预订。 如果你是项目组新来的小弟,那么恭喜你,每天抢订会议室的任务就光荣的...

3305
来自专栏牛客网

美团点评2019届机器学习/数据挖掘算法实习生一面

3.14网申的(北京,基础研究部门),3.20笔试。二十多天没消息,然后今天(4.12)下午接到美团面试电话,当然是前两天约好的,面试官大概迟到了十多分钟。 Q...

4256

扫码关注云+社区

领取腾讯云代金券