Functional Design in Clojure - 第003集:井字棋REPL 封面

第003集:井字棋REPL

Episode 003: Tic-Tac-REPL

本集简介

内特尝试将井字棋的“游戏引擎”变成一个可以与朋友一起玩的真实应用。 来玩游戏吧! 如何跟踪游戏状态的变化? 使用recur传递未来循环的引用 游戏循环:读取输入、评估、打印新棋盘、循环。 “我突然意识到我们基本上是在写一个REPL。” “我们有了井字-REPL” 如何获取用户输入?如何确保输入正确? “它会一直骚扰不配合的用户,直到他们输入正确的内容” 输入循环:读取、验证、出错时循环、成功时返回 保持逻辑纯净!将解析和验证函数分离出来。 “最好把它藏在一个函数里!” 用最简化的I/O函数串联纯逻辑部分。 “我不喜欢有烤箱出现,因为它们很难放进我的测试用例中。” 为那些纯逻辑部分编写单元测试。(没人喜欢被模拟。) I/O是一种副作用! “每次重新定义这些东西时,我都感觉自己在深入Clojure的内部,做一些稍微不合法的事情。” 使用关键字作为错误代码很巧妙 “你没有不同种类的nil。你只有一个。它就是那个‘不’。” 可以使用元组,第一个元素总是关键字,第二个是“详情”数据 本集出现的Clojure: read-line string/split swap!和reset! loop和recur let与loop 关键字 nil双关

双语字幕

仅展示文本字幕,不包含中文音频;想边听边看,请使用 Bayt 播客 App。

Speaker 0

欢迎来到函数式设计讨论会。

Welcome to functional design enclosure.

Speaker 0

我是克里斯托弗·纽曼。

I'm Christophe Newman.

Speaker 1

我是内特·琼斯。

And I am Nate Jones.

Speaker 0

每周我们都会聚在一起,讨论一个软件设计问题,以及如何用函数式编程和Clojure语言来解决它。

Each week we get together and we discuss a software design problem and how we might solve it using functional programming and the Clojure programming language.

Speaker 0

那么,内特,这周我们要讨论什么话题?

So, Nate, this week, what are we talking about?

Speaker 1

嗯,克里斯托弗,我真的很喜欢上周讨论的井字棋游戏引擎,以及我们如何用函数式编程(不能改变任何数据)来解决这个问题,而不是用面向对象编程(所有东西都隐藏在类后面)。

Well, Christophe, I really enjoyed talking about Tic Tac Toe, the game engine last week and how we would approach how we would approach solving that problem using functional programming where we can't mutate any data, as opposed to object oriented programming where everything is hidden behind the class.

Speaker 1

我喜欢那样,但完成之后,我们基本上可以在REPL或文件中玩游戏或模拟井字棋游戏,但我们无法真正互相对战,因为我不能先走一步,然后你走一步,我再走一步。

I like that, but once we were done with that, we could essentially play a game or we could simulate a game of tic tac toe in the REPL or in a file, but we couldn't really play tac toe against each other because I couldn't make a move, and then you make a move, then I make a move.

Speaker 1

所以我认为接下来可以讨论的是:如何基于上周的逻辑,把它变成一个真正能让我们互相对战的游戏。

And so I think that that might be a good thing to talk about is how would we take the logic that we had last week and make it into an actual game that you and I can play against each other.

Speaker 0

对。

Right.

Speaker 0

是啊。

Yeah.

Speaker 0

你说'游戏引擎'时真把我逗笑了,因为这听起来比我们最终实现的——仅仅是一个函数——要高大上多了。

And you're cracking me up when you said game engine because boy, that just sounds so much more than what we ended up with, which was a function.

Speaker 0

这就是我们遇到的美妙之处:如果你能写出一个推进游戏状态的函数,那你就拥有了游戏引擎的雏形。

And that's one of the beauties that we run into is if you can write a function that can advance the game state, then you have a have the beginnings of a game engine.

Speaker 0

那么,我们该如何将这个函数封装成一个应用程序,让我们能真正玩井字棋,而不仅仅是在REPL里输入数据呢?

And so, how do we how do we take that function and wrap it up in an application so we can actually play tic tac toe and not just feed it things in the REPL?

Speaker 0

没错。

Yeah.

Speaker 0

这听起来确实很有意思,你知道,这就像是实现一个真正游戏的下一步。

That sounds like a really interesting thing that, you know, it's kinda like the next step, right, for for an actual game.

Speaker 1

是啊。

Yeah.

Speaker 1

完全同意。

Totally.

Speaker 1

我是说,我们不需要搞得太花哨。

I mean, we don't have to make anything fancy.

Speaker 1

不需要做成网页应用或移动应用之类的。

We don't have to make it like a web app or a mobile app or whatever.

Speaker 1

就假装你我正在同一台电脑上玩闭包...抱歉,是玩井字棋对战。

Let's just pretend you and I are playing Closure sorry, playing tic tac toe against each other on the same computer.

Speaker 1

我们需要一个能运行的应用,它能显示游戏并让我们下棋。

We need an application we can run, and it'll show us the game and let make moves.

Speaker 1

所以我在想,游戏最基础需要什么?

So I don't know, what's the base thing you need in a game?

Speaker 1

你需要一个游戏循环。

You need a game loop.

Speaker 1

就是那个实际会显示游戏的循环。

You need the actual loop that that will, you know, show you the game.

Speaker 1

我是说,我记得小时候在DOS系统上玩大富翁游戏,它会显示游戏界面,然后在底部有个提示问'你接下来想做什么?'

I mean, I remember playing Monopoly when I was a kid on in DOS, you know, and it would show you the game, and it would just have a prompt at the bottom and say, what do you wanna do next?

Speaker 1

所以我们就做个类似这样足够简单的吧。

And so let's just do something like That's simple enough.

Speaker 0

当然。

Sure.

Speaker 0

那我们得先打印出棋盘,然后询问'你的下一步是什么?'

So we gotta print out the board and then ask like, what's your move?

Speaker 0

可能还需要打印出当前轮到谁了。

Like probably, oh, maybe print out whose turn it is.

Speaker 0

对吧?

Right?

Speaker 0

这样会比较有帮助。

That would it's be like helpful.

Speaker 0

X。

X.

Speaker 0

对。

Yeah.

Speaker 0

X,该你走棋了。

X, take your move.

Speaker 0

然后我们输入行号和列号。

And then we type in like the row and then the column.

Speaker 1

是的,那

Yeah, that

Speaker 0

然后按回车。

makes And then hit enter.

Speaker 1

没错,完全正确。

Yeah, totally.

Speaker 1

然后游戏会显示新棋盘或者宣布'嘿,Nate赢了'。

And then, you know, the game would show us either the new board or would say, hey, you know, Nate won.

Speaker 0

哎呀,你彻底搞砸了,Nate赢了。

Boy, you really blew it, Nate won.

Speaker 0

你去了一个不存在的格子,415这个坐标在这个空间里根本不存在,对吧?

You went to a nonexistent square, you know, 415 just does not exist in this coordinate space, right?

Speaker 1

对,或者输入'我想退出'也无效。

Right, or I want to quit does not work either.

Speaker 0

我想退出。

I want to quit.

Speaker 0

让它停下。

Make it stop.

Speaker 0

哦,求求让它停下不是有效输入。

Oh, please make it stop is not a valid input.

Speaker 1

对。

Right.

Speaker 1

所以,好吧。

So, okay.

Speaker 1

那么,你知道我们怎么在闭包里实现循环呢?

So so, you know, how how how would we do a loop in closure?

Speaker 0

是的。

Yes.

Speaker 0

我记得第一次遇到这种情况时很困难,因为我习惯了使用while循环。

I remember the first time I ran into this, it was difficult for me because I was used to while loops.

Speaker 0

对吧?

Right?

Speaker 0

所以如果我回到命令式编程,我会想,好吧,我有一个while循环直到条件满足。

So so if I go back to imperative, I would think, okay, I have a loop while not done.

Speaker 0

对吧?

Right?

Speaker 0

所以我们会有个布尔值,就像C语言里的标志位,嗯哼。

So we'd have like a bool, like a boolean flag or, you know, for in c, like an Uh-huh.

Speaker 0

你知道,while循环条件不满足时就执行操作,直到某个条件将标志设为true,循环就退出。

You know, while not done, and then you do stuff, and then something decides to set that done flag to true, and then the while loop exits.

Speaker 0

简单。

Easy.

Speaker 0

对吧?

Right?

Speaker 0

所以,是的。

So so, yeah.

Speaker 0

既然我们不能改变,如果我们设置一个完成标志,我们就无法改变它,那我们该怎么做呢?

How do we how do we do that since we can't change if we if we make a done flag, we can't change it.

Speaker 0

对吧?

Right?

Speaker 0

那我们该怎么办?

So what do we do?

Speaker 1

嗯,我的意思是,Closure有一个叫做原子的东西,你知道吗?

Well, I mean, Closure has a thing called an atom, you know?

Speaker 1

所以我们可以做的是,我们可以把游戏状态或者我们可以让原子只是一个布尔值,真或假,我们可以把它放在循环的顶部。

So what we could do is we could take the game state or we could have the atom be just a Boolean, true or false, and we could have that be at the top of our loop.

Speaker 1

CloakShake真的想要吗?

Does CloakShake actually and want

Speaker 0

然后我们交换值,或者重置,我想,reset是你使用的函数。

then we swap the value, or reset, I think, is a function you use, reset.

Speaker 0

所以我们基本上就像在while循环中检查done标志,然后重置这个值。

And so we basically have like while at done, you know, and then and then we just reset the value.

Speaker 0

当然。

Sure.

Speaker 0

所以这是可变的方式。

So that that's mutative.

Speaker 0

如果我们想避免可变性,有其他方法吗?

If we wanna avoid mutation, is there a way to do it?

Speaker 1

是的。

Yeah.

Speaker 1

完全正确。

Totally.

Speaker 1

所以要用到实际名为loop的函数。

So it's it's using the actual function called loop.

Speaker 1

所以loop函数只需要

So loop just takes

Speaker 0

想象一下。

Imagine that.

Speaker 1

是啊,想象一下。

Yeah, imagine that.

Speaker 1

这很棒。

It's wonderful.

Speaker 1

这有点棘手,因为最后需要做一些特殊处理(称为recur),具体操作是:先启动循环,传入几个初始值(有点像let块),最后如果想再次进入循环,只需调用recur。

It's a little bit tricky because you have to do something special at the end, which is called recur, but what you do is you start your loop, you put in a couple of values that you want to initialize, kinda like a let block, and then at the end, if you want to go back into that loop again, you just call recurred.

Speaker 1

本质上它会直接跳回循环顶部,不进行实际的递归操作——尽管名称有这种暗示。

Basically, it jumps straight up to the top without doing any sort of actual recursion, even though that's what it's kind of implied.

Speaker 0

对。

Right.

Speaker 0

我记得第一次用loop和recur时感觉有点奇怪,因为它有点像let块——你可以用一组初始值启动循环,比如我们可以设置loop的done标志并初始化为false。

I remember the first time I used loop and recur, it it was a little odd, right, because it's kinda like a let block where you can you can start with one set of values like you so for example, let's just say we'd have loop and then we'd have the done flag and we would initialize it to false.

Speaker 0

当我们调用recur时,必须传入该变量的新值。

And then when we call recur, we have to pass what the new value is for that.

Speaker 0

这样它会回到相同的位置,然后我们会在下次循环时传入一个done标志为true或false。

So it's gonna go back to the same place and then we're gonna pass a done flag of true or a done flag of false in for the next time through the loop.

Speaker 0

对吧?

Right?

Speaker 0

这样你就能决定下一次循环迭代时的变量值。

So it allows you to decide what the value is gonna be for the next loop iteration.

Speaker 1

是的。

Yeah.

Speaker 1

而且退出循环的方式是...loop关键字本身并不会帮你完成这件事。

And and the way that you you exit the loop is the the loop keyword doesn't actually do it for you.

Speaker 1

你只需要测试条件,不再调用recur即可。

You would just test, just do not recur.

Speaker 1

只要不调用recur,就会退出循环。

If you just don't recur, you exit your loop.

Speaker 0

对。

Right.

Speaker 0

所以不像while循环那样会不断检查条件,而是由你决定是否要递归执行。

So unlike a while loop where it's like it's gonna kinda go back and test a condition, Like it's up to you to have a condition that either recurs or does not recur.

Speaker 0

因此如果不递归,就会退出循环。

And so if it does not recur, then you exit.

Speaker 0

对吧?

Right?

Speaker 0

就像直接执行完一样。

Like you just fall fall through.

Speaker 0

如果递归调用,就会回到循环顶部。

And if you do recur, well, then you go back to the top.

Speaker 0

我们将有一个主函数,其中包含这个2个参数。

So we're gonna we're gonna have a main function and it's gonna have this loop recur.

Speaker 0

所以我们需要知道是否已完成。

And so we're gonna need to know whether or not we're done.

Speaker 0

而且,我想我们也需要以某种方式跟踪棋盘状态。

And well, I think we need to keep track of the board somehow too.

Speaker 0

对吧?

Right?

Speaker 0

对。

Right.

Speaker 0

所以,

So,

Speaker 1

你知道,循环开始的部分可以是,比如这里是初始游戏状态,我们可以直接在那里初始化。

you know, part of part of the loop start would be, you know, here's the initial game state, so we could just initialize it there.

Speaker 1

然后我认为我们不需要一个完成标志。

And then I don't think we think need a done flag.

Speaker 1

如果我们不进行递归,自然就知道结束了。

If we just if we don't recur, then we know we're done.

Speaker 1

这有点隐式的意思。

It's kind of implicit.

Speaker 0

当然。

Sure.

Speaker 0

所以,但一旦我们需要知道什么时候还没结束,对吧?

So, but once So we need to know when we're not done then, right?

Speaker 1

没错。

Correct.

Speaker 0

我们需要某种方式来检测游戏是否结束。

We need some way of detecting, is this game over?

Speaker 0

那我们能不能写一个函数,传入游戏状态就能判断是否有赢家?

So how about like a function that we can pass at the game state and it will tell us if there is a winner?

Speaker 0

你觉得怎么样?

How about that?

Speaker 1

可以。

Yeah.

Speaker 1

完全没问题。

Totally.

Speaker 1

这样可行。

That works.

Speaker 0

那么回到我们之前定义的游戏状态,你基本上有这个映射或记录,它包含棋盘的二维表示以及当前轮到谁下棋。

So so back to our our game state we defined before, you kinda have this map or record and it has this like two d representation of the board and whoever's turn it is.

Speaker 0

所以我们可以编写一个函数来遍历棋盘,寻找三个连续的'X'或三个连续的'O'。

So so we could write a function that kinda goes through the board and it looks for three x's in a row, three o's in a row.

Speaker 0

这本身可以是一个有趣的函数,我们现在就假设它有魔法般的功能。

That could be its own interesting function that we're we'll just say right now there's magic.

Speaker 0

对吧?

Right?

Speaker 0

你编写这个函数,它会尝试所有可能性。

You you write this function, it tries all the possibilities.

Speaker 1

剩下的就留给读者作为练习。

Left is an exercise of the reader.

Speaker 0

剩下的留给读者作为练习,它会返回获胜方。

Left is an exercise reader and it returns who won.

Speaker 0

对吧?

Right?

Speaker 0

所以它可以返回x、o或nil,表示目前还没有人获胜。

And so it it could return like an x, an o, or nil, like nobody's won yet.

Speaker 0

因此,如果返回nil,我们就知道需要递归处理。

So, if it returns nil, we know we need to recur.

Speaker 0

对吧?

Right?

Speaker 1

对。

Right.

Speaker 1

但在我们递归之前,或者说实际上在递归前,我们需要获取下一步走法,对吧?

But before we do, or actually, before we recur, we need to get the next move, right?

Speaker 1

还没人获胜,我们需要继续走棋。

No one's won yet, we need to continue on moving.

Speaker 1

所以我们需要先打印游戏状态,然后按你说的读取那两个整数。

So, we need to print the game, and then we need to, you know, as you said, read those two integers.

Speaker 0

我明白了。

I see.

Speaker 0

所以我们需要

So we need to

Speaker 1

两个函数。

two functions.

Speaker 0

好的。

Okay.

Speaker 0

所以我们有一个循环,这个循环接收状态。

So we have a loop, and loop takes state.

Speaker 0

对吧?

Right?

Speaker 0

而状态就像是初始状态,比如一个全新的游戏。

And state is just like the initial state of, like, the like a brand new game.

Speaker 0

然后我们打印出棋盘。

And then we print out the board.

Speaker 0

所以我们有一个函数,只要传入棋盘数据就能打印出棋盘状态。

So we have a function that can print out the board if we hand out the board.

Speaker 0

然后我们还有另一个函数。

And then we have another function.

Speaker 0

如果我们再写一个叫'获取回合'的函数怎么样?

How about if we make another function called like get turn?

Speaker 1

对吧?

Right?

Speaker 1

是的。

Yeah.

Speaker 1

那会是个好主意。

That would be a good idea.

Speaker 1

因为获取回合这个操作,我感觉底下会涉及很多复杂逻辑。

Because getting the turn, I have a feeling there's gonna be a lot of complexity underneath that.

Speaker 1

我的意思是,你需要读取输入,验证它,还要确保输入的是两个数字。

I mean, you gotta read it, you gotta validate it, you have to make sure that there's two numbers there.

Speaker 1

这里面有很多事情要做,所以我们不想把所有那些都放在循环里。

There's a whole bunch of things, so we don't wanna put all that in our loop.

Speaker 1

最好把它们封装到一个函数里。

Much better to tuck it away in a function.

Speaker 0

好的。

Okay.

Speaker 0

那就叫getTurn吧。

So getTurn.

Speaker 0

然后getTurn会返回一个行和列的值给我们。

And then getTurn's gonna give us kind of a row and a column back.

Speaker 0

接着我们会把这个应用到游戏状态上。

And then we're gonna apply that to the game state.

Speaker 0

然后我们会调用那个判断胜负的get winner函数。

And then we're going to call the is, you know, is winner or get winner function.

Speaker 0

如果没有赢家,我们就用这个新的游戏状态进行递归。

And if there's no winner, we recur with this new game state.

Speaker 0

对吧?

Right?

Speaker 0

我们通过递归传递这个新的游戏状态,这样循环的下一次迭代就有了这个新状态。

We we pass recur this new game state, so the next iteration of the loop has this new.

Speaker 0

这就是我们之前讨论过的引用传递链,对吧?

So that's the bucket brigade of references we talked about before, right?

Speaker 0

我们最初持有一个空游戏状态的引用,然后调用回合函数得到新游戏状态,接着通过递归传递这个状态,不断向前传递。

We like started with a reference to the empty game state, then we called like our turn function, which gave us a new game state, and then and then we pass that with recur, so we pass it forward.

Speaker 0

我们像传递接力棒一样把引用传递给循环的下一次调用。

We bucket brigade the reference forward to the next invocation of the loop.

Speaker 0

对吧?

Right?

Speaker 1

是的。

Yeah.

Speaker 1

没错。

Definitely.

Speaker 1

好的。

Okay.

Speaker 1

所以我认为目前为止最有趣的函数将是输入函数,因为打印游戏棋盘基本上就是遍历所有行并打印出来,然后打印出当前轮到谁以及可能的时间或其他类似信息。

So I think by far the most interesting function here is going to be the input function, because printing the game board is gonna be basically going through all of the rows and printing them out and then printing out whose turn it is and maybe the clock time or something like that.

Speaker 1

也许它还会列出所有之前的走法。

Maybe it has a listing of all the previous moves.

Speaker 1

但这些基本上都是将数据结构转换成字符串。

But that's all basically taking a data structure and turning it into a string.

Speaker 1

所以,是的。

So Yeah.

Speaker 1

我认为讨论如何从用户那里获取输入要有趣得多。

I think it's far more interesting to talk about getting input from the user.

Speaker 1

那么,你曾经在Closure中处理过终端输入吗?

So, have you ever done input from the terminal in Closure?

Speaker 1

我想我从来没有做过。

I don't think I ever have.

Speaker 1

现在应该有个函数。

Think there's a function now.

Speaker 1

哦,你有吗?

Oh, you have?

Speaker 0

哦,酷。

Oh, cool.

Speaker 0

是的。

Yeah.

Speaker 0

我有针对这类学习练习的函数。

I have one for these kind of learning exercises.

Speaker 0

哦。

Oh.

Speaker 0

通常来说,我都是从标准输入读取的,比如我写的小工具在Linux管道中获取数据时。

Normally, know, I've read from standard in, so if if I've written a little utility that's getting things in a pipeline in Linux or something.

Speaker 0

不过确实有个叫read line的函数,顾名思义就是读取一行。

But, yeah, there's this there's a function you can call called read line, which shockingly reads a line.

Speaker 0

完全按照字面意思执行。

Does exactly what it says.

Speaker 0

算是吧,嗯。

Some some yeah.

Speaker 0

是的。

Yes.

Speaker 0

没错。

Yes.

Speaker 0

我知道。

I know.

Speaker 0

这种命名方式很棒对吧?

You gotta love the naming, right?

Speaker 0

它会读取一行内容并以字符串形式返回,然后你就可以随意处理了,明白吗?

And so it reads a line and gives it back to you as a string and then you can do what you will with it, right?

Speaker 0

所以我们大概需要打印棋盘,再打印一个提示符。

So so we'd probably like print the board, print a prompt.

Speaker 0

所以这个像'get turn'这样的函数,在我的想象中,它应该会有一个循环。

So this function like get turn in my imagination, well, it's gonna have a loop.

Speaker 0

对吧?

Right?

Speaker 0

因为它需要不断骚扰这个不配合的用户,直到他们输入正确的内容或有效的移动。

Because it's gonna have to keep harassing this this non compliant user until they type the right thing in or type a valid move in.

Speaker 0

对吧?

Right?

Speaker 0

所以它就是要不停地烦他们。

So it's just gonna it's just gonna pester them.

Speaker 0

所以它必须有一个循环,才能一遍又一遍地烦他们,直到他们配合程序。

So it's gonna have to have a loop in order to pester them over and over again until they get on board, get with the program.

Speaker 1

是啊。

Yeah.

Speaker 1

所以我们有了循环中的循环,实际上我突然意识到,我们基本上就是在写一个REPL,一个非常简单的REPL。

So we have a loop within the loop, and and it actually is occurring to me that we're basically writing a REPL, a very simple REPL.

Speaker 1

它无法解析任何类型的代码,除了会接受一个整数元组。

It is unable to parse any sort of code except for it will accept a tuple of integers.

Speaker 0

所以,你是说它读取输入,然后像游戏状态一样评估它,接着打印新的状态,然后循环。

So, you're saying it reads input and then it evaluates it like the game state, and then it prints the new, and it loops.

Speaker 0

R e p l。

R e p l.

Speaker 0

对吧?

Right?

Speaker 0

是的。

Yes.

Speaker 0

我们有了井字棋的REPL。

We have the tic tac toe REPL.

Speaker 0

井字棋REPL。

Tic tac REPL.

Speaker 0

没错。

Yes.

Speaker 0

没错。

Absolutely.

Speaker 0

好的。

Okay.

Speaker 0

所以在这个循环里,我们我们要读取这行内容。

So so in this loop, we we we wanna read out this line.

Speaker 0

我们需要将其解析为两个整数。

We want to parse it into two integers.

Speaker 0

然后我们要检查这些整数的有效性,比如是否在有效范围内?

Then we wanna check those integers for validity, like are they in range?

Speaker 0

那个位置是否已经被占用了?

Is that space already occupied?

Speaker 0

对吧?

Right?

Speaker 0

所以这个函数可能需要传入当前游戏状态,这样它才能知道一些情况。

So this thing probably needs to this function probably needs to be passed the current game state so it can know a thing or two.

Speaker 1

是的。

Yeah.

Speaker 1

但你刚才列举了四件不同的事情。

But if you so you just listed off like four different things there.

Speaker 1

所以你需要进行一些输入输出操作,读取用户需求,然后进行数据转换和验证后才能返回结果。

So you wanna do some IO, you wanna read what the user wants, and then you need to do some coercing and some validation before you return it.

Speaker 1

我认为你可以把这些功能都放在同一个函数里,我以前写过这样的函数,大概八行代码就能完成所有需求。

And I think you could do all those things in the same function, I've written functions like this before, where the function is like eight lines long, and it does everything you need.

Speaker 1

基本上就是你说'给我一个蛋糕',它就会完成所有烘焙工作,然后把蛋糕交给你。

You basically say, Give me a cake, and it does all the baking needed to give you the cake, and it gives you the cake back.

Speaker 1

但这样做的问题是很难测试,因为在中间某个环节你必须要有烤箱。

But the problem with that is it really is difficult to test because somewhere in the middle there, you had to have an oven present.

Speaker 1

哦,确实。

Oh, yeah.

Speaker 1

而且我不喜欢引入烤箱,因为它们很难放进测试用例里。

And I don't like having ovens present because they're hard to put in my test cases.

Speaker 0

对。

Right.

Speaker 0

这里的烤箱指的是什么?

What's the oven in this case?

Speaker 0

帮我理解一下

I Help me with this

Speaker 1

我会说烤箱就是那个粘乎乎的有机体——用户本身,你必须从实际的终端读取输入。

would say the oven is the actual sticky organic thing called the user where you have to actually read input from the actual terminal.

Speaker 0

哦,对。

Oh, right.

Speaker 0

是啊。

Yeah.

Speaker 0

所以这个read line会搞乱你,对吧?

So this read line like screws you up, right?

Speaker 0

因为它会产生副作用。

Because like it's a side effect.

Speaker 0

那你打算怎么测试这个?

And so how are you gonna test this?

Speaker 0

我知道Nate,你应该能模拟read line。

I know Nate, you should be able to mock read line.

Speaker 0

这才是你该做的

That's what you should

Speaker 1

能够做到的。

be able to do.

Speaker 1

嘿,你知道吗,其实你可以的。

Well, What do you you know, you actually can.

Speaker 1

我觉得ReadLine是...抱歉,我是说,不能模拟ReadLine吗?

I think ReadLine is, sorry, can, I mean, I'm sorry, not mock ReadLine?

Speaker 1

我的意思是,你可以随意模拟ReadLine,但你可以重新定义标准输入。

I mean, you can mock ReadLine all you want, but you can redefine what standard end is.

Speaker 1

这在Clojure里算是比较高级的技巧了。

That's kind of a more advanced thing in closure.

Speaker 1

但我觉得假装我们做不到这点,你可能想做的也许是

But I think pretending we can't do that, I think what you want to Maybe do

Speaker 0

我们甚至不想那样做,对吧?

we don't even want to do that, right?

Speaker 0

比如,我们该怎么做?

Like, what should we do?

Speaker 1

每次我重定义这些东西时,总觉得自己在深入Clojure的底层,做些有点违规的事

Every time I redefine one of those things, always feel like I'm reaching down into the bowels of closure and doing something moderately illegal.

Speaker 1

但在这种情况下,我们并不是要测试是否这样做,我们是在测试ReadLine

But in this case, we don't really want to test if we're doing that, we're testing ReadLine.

Speaker 1

我们不需要测试ReadLine

We don't need to test ReadLine.

Speaker 1

我认为可以相信readline是正常的,所以我们真正要做的是:无论readline返回什么,都要测试从那里开始直到返回的所有逻辑

I think we can trust that readline works, and so what we really wanna do is to have whatever readline we give back to us test everything from there until we hand it back.

Speaker 1

这才是重点

That is the bulk.

Speaker 1

这就是我们正在编写的代码。

That's the code that we're writing.

Speaker 1

我们不想测试那些并非我们编写的代码。

We don't wanna test code that we're not writing.

Speaker 1

我们想测试的是我们正在编写的代码。

We wanna test the code that we're writing.

Speaker 1

因此,我们需要分离出来,以便

And so, we need to separate So that

Speaker 0

这个想法是将所有这些不同部分拆解成各自纯粹的问题,对吧?

the idea is we separate out all these different pieces into their sort of pure problems, right?

Speaker 0

所以我们想写一个函数,它接收读入的字符串然后对其进行处理?

So we wanna write a function that, what, takes the string that was read in and then does something to it?

Speaker 1

对。

Yeah.

Speaker 1

我认为合理的做法是接收字符串并返回一对整数或错误,这样你就能打印那个错误信息。

I it'd be reasonable to take a string and return either a pair of integers or an error so that you can you can print that error.

Speaker 1

对吧?

Right?

Speaker 1

因为如果有人输入,比如我想去0号空间和a区,我们需要提示'第二个不是数字'。

Because if if if someone enters, you know, I wanna go in space zero and a, we wanna say, hey, that second number was not a number.

Speaker 1

明白吗?

You know?

Speaker 1

我们不想只是

We don't wanna just

Speaker 0

好的。

Okay.

Speaker 0

提示'无效输入'就很酷。

Say invalid It's super cool.

Speaker 0

对吧?

Right?

Speaker 0

因为这样我们就有一个函数,它接收字符串并返回可能是'无效'这样的关键词,或者返回一对整数。

Because then we have a function, it takes a string and it's gonna give us back maybe a keyword that's like invalid or it's gonna give us a pair of integers.

Speaker 0

对吧?

Right?

Speaker 0

我们可以检查,它会返回nil,而nil意味着他们没有提供有效内容。

We can look we can look at or it's gonna give us back nil and nil means like, nope, they did not give us something about.

Speaker 0

所以现在你可以写一堆单元测试来处理所有这些不同的字符串,比如为场景A、场景B、场景C分别传入字符串的测试,完全不需要模拟reline或其他复杂操作。

So now you can write a bunch of unit tests that take all these different strings, right, like a unit test that takes a string for scenario a, a string for scenario b, a string for scenario c, and you don't need to mock reline or any of that craziness.

Speaker 0

你可以真正把解析问题隔离到一个函数里:当解析失败时返回nil,当至少能解析出两个整数时就返回整数对。

You can really isolate out the parsing problem into a function that gives you nil for nothing really worked out, and then it gives you two integers where, hey, at least I could get some integers.

Speaker 0

对吧?

Right?

Speaker 1

是的。

Yeah.

Speaker 1

完全正确。

Totally.

Speaker 1

而且关键字是表示错误的好方法,因为这样你就可以在前端把关键字翻译成特定语言的错误信息。

And then and then the keyword is a nice way of representing errors because, you know, you don't you wanna be able to have the keyword translated into some, you know, language specific error message at the front end.

Speaker 1

你不想让这些错误信息混杂在你的代码逻辑中。

You don't you don't wanna have that generated back in some of the bells of your code.

Speaker 0

确实。

Sure.

Speaker 0

所以在这种情况下,nil可以表示解析失败,而有效则代表解析成功。

So in this case, it could be nil be for like, nope, didn't work out versus, hey, it did work out.

Speaker 0

但如果你需要更细致的区分,比如只输入了一个数字而没输入第二个。

But if you went in more nuance, like, oh, well, typed one number, but you didn't type two.

Speaker 0

或者输入了三个数字但本应只输入两个。

Or you typed three numbers, but you should have only typed two.

Speaker 0

你可以让它返回关键字而非nil,以区分不同的错误场景。

You could you could have it return a keyword instead of nil for, like, the different error scenarios.

Speaker 1

是啊。

Yeah.

Speaker 1

完全正确。

Totally.

Speaker 1

所以一旦你有了这些,事情就变得有趣起来了。

So then once you have those, then you can, then it becomes interesting.

Speaker 1

实际上它变成了你需要真正验证的东西,要对照当前游戏状态来验证。

It actually becomes, you know, something you need to actually validate against, you know, with the current game state, you know.

Speaker 1

比如说,如果你说要去第一行第八列,这两个数字本身是有效的,但我们正在玩的井字棋棋盘只有3x3大小,所以这个位置是无效的。

Is this, you know, if you say I wanna go row one and column eight, you know, those are two valid integers, but the Tic Tac Toe board, at least the one we're working with, is only three by three, so that's not invalid.

Speaker 1

因此,我们需要有某种方式来检查这种情况,但同时我们还需要检查,比如内特已经在中心格下过棋了。

So, we need to have some way of checking for that, but we also need to check for, you know, hey, Nate already played in the center square.

Speaker 1

克里斯托夫,你不能这么下。

You know, Kristoff, you can't do that.

Speaker 1

所以我们还需要有方法来检查这种情况。

So we need to have a way of checking for that too.

Speaker 0

所以我们可以调用一个函数,比如叫is_valid_turn,它需要接收什么参数?

So like a function we could call, you know, is valid turn, and then it takes what?

Speaker 0

游戏状态,以及行号和列号。

The game state, and then the row in the column.

Speaker 0

是的,确实如此。

Yeah, definitely.

Speaker 0

或者说回合元组,如果你愿意这么称呼的话。

Or or the turn tuple, if you will.

Speaker 0

然后它会返回true,如果是有效的话。

And then it it returns like true, if it is.

Speaker 0

我会说它

I would say it

Speaker 1

应该返回元组本身或一个关键字。

should return the tuple itself or a keyword.

Speaker 1

就像,如果我们所有函数都遵循相同的模式,最后我们就可以判断,我们得到的是关键字还是元组?

Like, think if we follow the same pattern on all of our functions, then at the end, we can say, did we get a keyword out of this or or a tuple?

Speaker 1

如果得到的是元组就可以继续,因为最终你需要把控制权交还给顶层循环,这才是真正推动游戏状态前进的部分。

And you can keep going if you had a tuple because in the end, you gotta hand the tool back to the top level loop because that's what's actually gonna advance the game state.

Speaker 0

哦,我明白了。

Oh, I see.

Speaker 0

所以这个从主循环调用的get_input函数只有一个任务,就是返回一个元组或关键字。

So this this get input function that is called from the main loop has one job, and its job is to get us a tuple back or a keyword back.

Speaker 0

如果它返回的是关键字,那就说明出问题了。

And if it gets us a keyword back, something went wrong.

Speaker 1

对。

Yeah.

Speaker 1

也许它甚至可以

Maybe it could even It

Speaker 0

不会推进游戏状态。

won't advance the game state.

Speaker 1

对。

Yeah.

Speaker 1

也许它甚至能处理快速返回quit关键字的情况,你知道,如果遇到这种情况,它可以处理所有输入。

Maybe it could even handle the quick case where it gives back a quick to, you know, keyword if it's that's know, we can it could handle all input.

Speaker 0

哦,我明白了。

Oh, I see.

Speaker 0

对。

Right.

Speaker 0

所以这个解析器实际上可以不只是返回整数,因为我们已经让它能返回关键字,比如当你输入'请停止'时,它可以返回退出关键字。

So this parser could actually instead of just giving integers, this parser could because we have it returning a keyword, if you type please make it stop, then it could return the quit keyword.

Speaker 0

然后我们就知道,哦,我们收到了一个关键字。

And then and then we go, oh, we got a keyword.

Speaker 0

我们不需要验证它。

Let's not try to validate that.

Speaker 0

直接返回它就好。

Let's just return that.

Speaker 0

我们通过不在这个输入循环中递归调用来实现返回。

And we return by not recurring in this in this loop for the input.

Speaker 0

对吧?

Right?

Speaker 1

是的。

Yeah.

Speaker 1

对。

Yeah.

Speaker 1

确实如此。

Definitely.

Speaker 1

好的。

Okay.

Speaker 1

那么一旦我们完成这一步,现在在顶层结构中,我们就会得到要么是一个关键词,要么是一对整数,我们可以利用这个结果来推进游戏状态,或者不进行递归调用,然后我们就完成了。

So then so once we have that done, so now now now at the in the top level, we have either a keyword or a pair of integers, and we can take that, advance the game state, or or not recur, and then we're done.

Speaker 0

我明白了。

I see.

Speaker 0

所以如果我们收到退出指令,就不进行递归,跳出循环后程序就结束了。

So if we get quit, we don't recur, and we fall out of the loop and we're done.

Speaker 0

如果我们收到带错误的关键词,可能会显示那个错误,然后重新回到循环顶部。

If we get a keyword with an error, maybe we display that error, and then we loop back to the top.

Speaker 0

我们以初始的游戏状态进行递归调用,因为没有任何变化发生。

We recur with the same game state we started with because, hey, there's nothing that changed.

Speaker 0

对吧?

Right?

Speaker 0

是的。

Yeah.

Speaker 0

如果我们得到一个元组,那我们就说,哦,好的。

And if we get a tuple, then we go, oh, okay.

Speaker 0

让我们应用这个回合,因为我们知道它是有效的,然后用新的游戏状态进行递归。

Let's let's apply the turn because we know it's valid, and let's recur with the new game state.

Speaker 1

对。

Yeah.

Speaker 1

这让我想起Scala里的一个概念,我记得有种叫做选项类型的东西,可以让你表示一个值要么是有效的,要么是一个错误。

There's one thing it reminds me of something that I remember from Scala where there was a way of you you could you could I think what they were called option types where you could actually have it so that it was either a valid value or an error.

Speaker 1

没错。

Right.

Speaker 0

Haskell中的Maybe也是同样的概念,选项或Maybe。

Maybes in Haskell, same thing, options or maybes.

展开剩余字幕(还有 60 条)
Speaker 1

是的。

Yeah.

Speaker 1

这感觉非常相似,除了因为实际上你不能把强制函数的输出(接收字符串并返回数字的那个)不加区分地直接喂给下一个函数。

This feels like it's very similar to that except for, you know, because you can't actually take the output of the coerce function, which takes the string and returns the numbers, and just feed it indiscriminately to the next function.

Speaker 1

它只能处理存在两个整数的情况。

It only can handle the fact that there's two integers.

Speaker 1

所以这几乎就像你需要在里面放一个小型反应器,接收一系列函数,传递有效值,并在遇到关键字时短路处理。

So it's almost like you need to have a little, like, reactor inside of there that is taking these taking a list of functions and, you know, passing valid values on and and shortcutting when there's a keyword passed out of one of them.

Speaker 1

没错。

Right.

Speaker 1

你明白我的意思吗?

You know what I mean?

Speaker 0

是的。

Yeah.

Speaker 0

当然。

Sure.

Speaker 0

我明白你的意思,通过nil短路机制,你可以用some来实现这一点,这很不错因为可以让所有东西都返回nil。

I know I mean, with nil punning, you can do that with some, which is kind of nice because you can have everything returned nil.

Speaker 0

nil某种程度上相当于我们的语言结构中没有可选类型。

Nil is kind of the we don't have options in our language construct.

Speaker 0

我们使用nil短路机制。

We use nil punning.

Speaker 0

但nil短路机制的缺点在于,它无法提供更多上下文信息。

But then the bummer about nil punning, right, is it doesn't give you much more context that.

Speaker 0

你没法

You don't

Speaker 1

真正区分不同类型的nil。

really have any different kinds of nil.

Speaker 1

你只有一种nil。

You just have one.

Speaker 1

就是那种'我早告诉过你'式的回应。

Just the it's the uh-uh, the, you know, the I told you so kind of response.

Speaker 1

是啊。

Yeah.

Speaker 0

对。

Yeah.

Speaker 0

完全同意。

Totally.

Speaker 0

完全同意。

Totally.

Speaker 0

没错。

Right.

Speaker 0

但其实也不算太糟,你知道,就是那个尝试进行解析的函数。

But it's it's not too bad to have, you know, your function that tries to do the parsing.

Speaker 0

如果它返回一个关键字,就不会递归了。

And if it returns a keyword, it doesn't recur.

Speaker 0

对吧?

Right?

Speaker 0

否则,它会进入另一个分支去检查有效性。

Otherwise, it goes to the other branch where it tries to check for validity.

Speaker 0

如果返回的是关键字,它就不会递归,而是直接返回给顶层。

And if if that returns a keyword, it doesn't recur and it hands that back to the top.

Speaker 0

否则,它会返回元组。

Otherwise, it returns to tuple.

Speaker 0

对吧?

Right?

Speaker 0

是的。

Yeah.

Speaker 0

没错。

Yeah.

Speaker 0

确实如此。

Definitely.

Speaker 1

这这这很有趣,我们想用关键字同时处理错误和传达指令,比如退出命令,因为你需要区分哪些关键字应该冒泡传递,哪些应该让用户重新输入。

It it it's interesting we wanna use keywords for both errors and for for communicating, like, so to speak, instructions like quit, because you need to know which which keywords should you let bubble up and which ones should you force the user through another input loop.

Speaker 1

我认为这是一个有趣的边缘案例。

I think it it it's an interesting like edge case.

Speaker 0

是的。

Yeah.

Speaker 0

关键字在这方面非常有用。

Keywords are super useful in in that regard.

Speaker 0

有时我会用一个元组,其中第一个元素总是关键字,这样你可以有一个'okay'关键字,然后第二个元素是任何需要附带的数据,你懂的。

Sometimes I've just had like a tuple where the first thing is always a keyword, and so that's like you could have an okay keyword, and then the second thing is any data that wants to come along for the ride, you know.

Speaker 0

因此如果第一个条件不成立,那必定是个错误,此时元组的第二部分会包含关于错误的更多细节。

So if the first thing is not okay, then it must be an error, and then the second part of the tuple is more more details about the error.

Speaker 0

这样一来就形成了这种优雅的对称结构:始终用关键字表示上下文,再用数据提供更多细节。

And so then that way you have this nice symmetric thing where you always have a keyword for context and then the data for more details.

Speaker 0

如果你愿意,可以通过函数传递这个。

And you can thread that through functions if you want.

Speaker 1

哦,是的。

Oh, yeah.

Speaker 1

这将是一种有趣的方式,让多个函数依次处理它,允许它们在过程中添加或不添加内容。

That would be an interesting way to thread it through multiple functions, allowing them to to add or or not add as it goes along.

Speaker 0

对。

Right.

Speaker 0

它们都需要知道如何拆解这个结构。

They would all have to know how to pull that apart.

Speaker 0

不过确实。

But yeah.

Speaker 0

是啊。

Yeah.

Speaker 0

你可以这样来组织它。

You could structure it that way.

Speaker 0

谁能想到井字棋游戏会这么有趣呢,做一个小小的井字棋游戏?

So who knew that tic tac toe could be so interesting, making a little tic tac toe game?

Speaker 0

这确实非常有趣,今天能继续讨论这个话题也很有意思。

It certainly is a lot of fun, and it's been fun continuing talking about that today.

Speaker 0

感谢大家收听我们的播客。

Thank you all for listening to our podcast.

Speaker 0

如果你想查看本期节目的相关笔记,可以访问我们的网站closuredesign.club。

If you wanna find our show notes about this episode, you can go to our website at closuredesign.club.

Speaker 0

你也可以在Twitter上通过@closuredesign联系我们。

And you can also hit us up on Twitter at closuredesign.

Speaker 1

我们的邮箱是feedback@closuredesign.club。

Our email is feedback@closuredesign.club.

Speaker 1

如有任何问题或反馈,请务必告知我们。

Please let us know if you have any questions or any feedback.

Speaker 1

我们很乐意听取您的意见,哪怕是像'嘿Nate,你得把麦克风操作得更好些'这样的建议。

We'd love to hear from you even if it's something like, oh, you need to, you know, operate your mic a little better, Nate.

Speaker 0

当然。

Sure.

Speaker 0

是啊。

Yeah.

Speaker 0

尽管音频质量存在问题。

Audio quality's issues notwithstanding.

Speaker 0

我们非常期待听到您的反馈。

We definitely love to hear from you.

Speaker 0

我们下周会再回来。

We will be back next week.

Speaker 0

在此之前,我们恳请您将您的IO与Logix业务分开。

And until then, we implore you to keep your IO out of your Logix business.

关于 Bayt 播客

Bayt 提供中文+原文双语音频和字幕,帮助你打破语言障碍,轻松听懂全球优质播客。

继续浏览更多播客