“写经”(Shakyō)是一种佛教实践,参与者会仔细手抄神圣的经文,这是一项需要高度专注和决心的冥想练习。
嘿!我从90年代就开始教编程了。这一切始于我在中学的时候——我的计算机老师竟然信任我来教我的同学编程。这有多酷,对吧?这些年我尝试了各种教学方法,我有一个很有趣的发现:初学者其实通过复制现成的代码学得更快。谁能想到啊?
大约在2015年,当我开始教不同的人学习AI编程时,我对此有了深切的体会。那时,Python已经成为很多AI工作的首选语言。说实话,一开始我对自己对Python的了解也不多,它的独特语法简直让我一时之间不太适应!
我敢说,如果你习惯于使用 C 或 JavaScript,那么一开始 Python 的那些古怪规则可能会让你感到头晕。想想看——你不再使用熟悉的花括号 {} 来定义代码块,而是用缩进来表示,这可能需要你重新调整思维方式。更别说那些复杂的列表引用了——不仅仅是标准的 [1][2],还有那些让你摸不着头脑的 [1:] 和 [-1]。你越熟悉其他语言,Python 的独特之处就越可能让你大吃一惊!
当我在日本国家先进工业科学技术研究院(AIST)和国家农业与食品研究组织(NARO)教授AI编程时,我试图让大家从复制代码开始。让我告诉你们——他们可不乐意了!这些研究人员本身就是各自领域的程序员,所以他们觉得,“别开玩笑了,我们早就过了复制基础代码的阶段了!”说得也有道理,对吧?
但更有趣的是——当他们真正开始编写程序时,这些聪明的研究人员都犯了同样的错误!他们在哪儿又栽了跟头呢?当然是一些基础问题——括号不对齐,乱七八糟的缩进——你知道,典型的萌新犯的错。
他们坚持下去,开始逐渐熟悉 Python 的风格,分辨了[,] 和 (,) 的区别,甚至还学会如何操作那些 numpy 数组。从复制代码开始能帮助你培养这种惊人的直觉——当遇到错误时,你能立刻判断出是缩进问题、括号不匹配还是函数调用出错。
当然,一旦你感到自在了,你就可以停止复制代码,但在开始时,你必须亲自体验会发生什么问题和你可能会犯的错误。这就是复制现有代码真正有用的地方!
最近,AI可以为我们编写大多数代码,这真是太神奇了,但它还不能完全注意到所有的细节。当一个小错误导致一场大灾难时,AI还不能聪明到能指出,“嘿,你的代码缩进有问题!”不过至少目前还做不到——再给它点时间吧!
你知道有趣的是什么吗?我在用 Delphi IDE 学习 Pascal 时没怎么抄代码,到现在还没搞清楚 ‘end’ 和 ‘end.’ 这两个关键字的区别。如果今天让我考一次 Pascal,我可能会挂大分!😄
复制代码就像学钢琴——你得先练好基本功。这不算创造性的工作,但它绝对是基础中的基础。如果直接跳到用 AI 辅助编码,你可能会写出自己也不太明白的代码。当程序运行得像闪电一样快,远超人类反应速度时,理解它们怎么工作就变得极其重要,特别是在关键应用里。
我也没写多少Lisp程序,所以最近让AI为我写Lisp代码。但跟着AI写的Lisp代码走,这真是一场冒险!我在复制AI的代码时,心里会想:“嘿,这段代码能不能再短一点?”有时候行得通,有时候电脑就会因为错误而骂我!
复制代码的真正价值在于学习经验——发现哪些能省略,哪些能简化,哪些必须保持原样。这可能看起来像是走弯路,但每当我学习一种新的语言或框架时,我总是从复制一些代码开始。
每种编程语言都很精细,很容易被误解。比如说Python:
a = [1,2,3] # 定义一个列表 a 包含元素 1, 2, 3
b = (1,2,3) # 定义一个元组 b 包含元素 1, 2, 3
这些看起来几乎一模一样,但差别巨大——‘a’可以改,而‘b’则是铁板钉钉,不能更改!
当Python行为异常时,我总是在REPL中检查。但这里有个坑——即使使用REPL也可能让你摸不着头脑!当你亲自把代码敲出来时,你真的能摸到其中的门道,这种理解能很好地应用到更复杂的操作。
底线是?不先看或修改代码,即使经验丰富的程序员也可能无法真正“掌握”。当然,使用 Emacs 或其他 Lisp 友好的编辑器会更方便,但这只是另一种方式的说法,表明它还没有真正成为你的习惯!🚀
为了向你展示初学者跳过代码复制会有多艰难,我将放下架子,分享我的 Lisp 初体验(我让 AI 代劳,而不是自己动手练习)。让我们试着用 Lisp 编写一个超级简单的“Hello World”程序。比如说,在 Python 中,它会是这样的小菜一碟:
print("Hello World")
在 Lisp 中,这会是这样的:
(print "Hello World")
name = "shi3z"
print("你好%s。"%name)
任何 Python 编程者都能轻松完成!但是如果没有足够的练习,即使是这样一个基本的程序也可能让人感到困惑。
我最近在看这段AI帮我写的Lisp代码。
(defun 处理用户输入函数 (input)
(format t "用户输入: ~A~%" input)
(let ((响应 (发送OpenAI请求结果 input)))
(if 响应
(format t "~A~%" 响应)
(format t "服务器没有返回响应。~%"))))
所以我想,“好吧!我就把‘name’设置成‘shi3z’,然后用format来显示。这就简单了!”我了解到,Lisp用‘let’给变量赋值,用‘format’来格式化,于是我试试看这个:
(let (name "shi3z") (format t "Hello ~A" name))
看看当我把这个东西扔进sbcl(Lisp的REPL)的时候发生了什么……哇哦!😅
* ((let 名称 "shi3z")
(format t "Hello ~A~%" 名称))
; 在: (LET 名称
; "shi3z") (FORMAT T "Hello ~A~%" 名称)
; ((LET 名称
; "shi3z")
; (FORMAT T "Hello ~A~%" 名称))
;
; 出现错误:
; 非法函数调用
;
; 编译单元完成,但有错误
; 出现 1 个错误条件
调试器在主线程 #<THREAD tid=259 "main thread" RUNNING {7008390603}> 上被调用:
执行带有错误编译的代码。
表达式:
((LET 名称
"shi3z")
(FORMAT T "Hello ~A~%" 名称))
编译时错误:
非法函数调用
输入 HELP 获取调试器的帮助
可调用的重试选项 (通过编号或可能的缩写名称):
0: [ABORT] 退出调试器,返回顶层环境
((LAMBDA ()))
源代码: ((LET 名称
"shi3z")
(FORMAT T "Hello ~A~%" 名称))
0]
我就问了下ChatGPT,它说我用“let”这个关键字用错了。
在 Lisp 中,你需要将变量名及其初始值作为成对的项放在一个列表中。这里修正后的版本如下:
(let ((name "shi3z"))
(format t "Hello ~A~%" name))
啊哈!所以不是 (let (name “shi3z”))
而是 (let ((name “shi3z”)))
。原来是因为双括号可以用来同时定义多个变量。挺酷的!
咱们试试看
* (let ((name "shi3z")))
; 在LET ((NAME "shi3z"))
; (NAME "shi3z")
;
; 捕获到 STYLE-WARNING:
; 变量 NAME 被定义但从未被使用过。
;
; 编译单元已完成
; 捕获到 1 个 STYLE-WARNING 条件。
NIL
这次没有错误!但我尝试用 format
来创建一个带有 name
的消息时……
* (format t "Hello ~A~%" name)
调试器在主线程 #<THREAD tid=259 "main 线程" 运行中 {7008390603}> 中因变量未绑定错误引发:
变量 NAME 未绑定。
输入 HELP 获取调试器的帮助,或者输入 (SB-EXT:EXIT) 退出 SBCL 环境。
可用的重试选择(可通过编号或缩写名称调用):
0: [CONTINUE ] 可使用 CONTINUE 重试使用 NAME。
1: [USE-VALUE ] 可使用 USE-VALUE 使用指定的值。
2: [STORE-VALUE] 可使用 STORE-VALUE 设置并使用指定的值。
3: [ABORT ] 可使用 ABORT 退出调试器,返回主界面。
(SB-INT:SIMPLE-EVAL-IN-LEXENV NAME #<NULL-LEXENV 环境>)
0] 3
砰!又出错了!然后我突然想起来——在Lisp中,let
与词法作用域绑定。一旦退出这个 let
,name
在下一个 format
中就不可用了。也许这样可以?
(let ((name "shi3z"))
(format t "嗨 ~A~%" name))
尝试一下……不行哦!😭
* ( (let ((name "shi3z"))
(format t "Hello ~A~%" name) )
)
; 在: (LET ((NAME "shi3z"))
; (FORMAT T "Hello ~A~%" NAME))
; ((LET ((NAME "shi3z"))
; (FORMAT T "Hello ~A~%" NAME)))
;
; 捕获到错误:
; 非法函数调用
;
; 编译单元完成
; 捕获到 1 个错误条件
调试器在 #<THREAD tid=259 "main thread" RUNNING {7008390603}> 报告错误:SB-INT:COMPILED-PROGRAM-ERROR
形式:
((LET ((NAME "shi3z"))
(FORMAT T "Hello ~A~%" NAME)))
编译时错误:
非法函数调用
输入 HELP 获取调试器帮助,或输入 (SB-EXT:EXIT) 退出 SBCL。
可调用的重新启动 (按编号或缩写名称):
0: [ABORT] 退出调试器,返回顶层。
((LAMBDA ()))
源代码: ((LET ((NAME "shi3z"))
(FORMAT T "Hello ~A~%" NAME)))
0]
但现在我能看出来问题在哪里了——你其实可以在定义变量后立即在‘let’中链式调用其他函数调用。这样的话会怎么样:
打印如下:
- (let ((name "shi3z"))
(format t "Hello ~A~%" name))
打印结果为:Hello shi3z,最后返回值为NIL。
耶!终于搞定啦!🎉
我有40年的其他语言编程经验,却在基本的Lisp上跌跌撞撞!这真的说明,我们可能觉得自己懂很多,但实际上可能一窍不通,特别是当我们跳过这样的基础练习,比如直接复制代码时。
当然,使用 Emacs 或其他对 Lisp 友好的编辑器可能会让我避免这些错误的闹剧,但这正是重点——如果你还没有掌握基础知识的精髓,那你还没有真正入门!😄
你想让我调整语气,还是详细解释任何技术细节?
共同学习,写下你的评论
评论加载中...
作者其他优质文章