#英文版 CS145 1#


1 Introduction


Later lecture summaries are likely to be more terse and less completethan this one.

Lecture module 01 for CS 145 Fall 2013 introduces course conceptsusing the programming language Haskell. CS 145 students will not berequired to program in Haskell (we will be using Racket for programming,introduced in the next lecture module). However, Haskell is a highly-expressivefunctional programming language, and works well in explainingconcepts. Although you will not be required to program in Haskell, youwill be required to understand programs written using it, and we encourageyou to experiment with programming in it.


1.1 Natural numbers

We will build our own representation of the natural numbers,which we will call Nat, and we will define someelementary arithmetic operations on this representation.We represent zero (0) by Z.The successor operation (adding one) will berepresented by S. Thus S Z is the representation ofone (1). S (S Z) is the representation of two (2). Theparentheses here are necessary to disambiguate; the first Sis not applied to the second S, but to the quantity S Z,which we put in parentheses to indicate that the second S isfirst applied to Z. Wedenote by Nat the set of objects representable in thisfashion. A Nat is either Z or S applied toa Nat. This is described in Haskell as follows:


          data Nat = Z | S Nat


A vertical bar | is commonly usedin computer science to mean "or" in some fashion.Here it is used to create two variants of the type Nat.Haskell does not use the representation of integers we have just described.It has its own built-in representation of integers, using notation we aremore familiar with: 1 + 2 is an expression whose value is3. We are not using Haskell’s built-in representation ofintegers in this section, in order to examine more closelywhat we mean when we do arithmetic.

The + operator in Haskell (and in mathematics) is an infix operator,because it appears between its two operands. The simplest way todefine functions in Haskell uses prefix notation, where thename of the function appears before its operands orarguments. This is similar to what is done with S above,but S is not a function; it is a data constructor.As we will see, Racket does not have infix operators;it uses prefix notation.

Our goal is to define addition for our representation ofnatural numbers, such that the following Haskell expressionwill add our representations of one and two:


          plus (S Z) (S (S Z))


We must define the function plus, which hastwo parameters, in such a way that itcorresponds to what we understand about addition of naturalnumbers. For example, the value of the above expressionshould be S (S (S Z)).

Here are some more examples of expressions and their values.


          plus Z Z = Z

          

          plus Z (S Z) = S Z

          

          plus (S Z) (S Z) = S (S Z)


We want a general way of defining plus x w for allNats x and w. We know thatw is either Z or (S y), where yis some Nat. This suggests the following rules,to be completed by filling in the question marks:


          plus x Z = ?

          

          plus x (S y) = ?


After a bit of thought, and the realization that it’s okay ifplus appears on the right-hand side of a rule, we cancomplete the definition.


          plus x Z = x

          

          plus x (S y) = S (plus x y)


This is a complete definition of plus in Haskell.Before we see how it is used, let’s take a closer look at it.It consists of two rules. The first rule says, informally,"When plus is applied to two values, and the second value isZ, then the result is the first value." This informalidea is expressed in Haskell as this line:


          plus x Z = x


In this line of Haskell,the two expressions that follow plus are patterns.The pattern x matches any value and gives it the name "x".That name can be used in the expression on the right-hand sideof the equal sign, which specifies how to compute the result.The pattern Z matches the literal value Z.

With the idea of patterns in mind, let’s take a look at the second rule,which informally says, "When plus is applied to two values,and the second value is S applied to something, then theresult is S applied to the result of applying plus tothe first value and that something." Here is the line of Haskell again:


          plus x (S y) = S (plus x y)


The pattern x on the left-hand side is explained above.The pattern (S y) on the right-hand side matches anyvalue that is S applied to something, and gives the name"y" to that something. So if the pattern (S y) is matchedagainst the value S (S Z), then the name "y" is given toS Z. The names x and y can be used in the resultexpression on the right-hand side.

Now that we understand patterns,let’s take a look at an example of computation:


          plus (S Z) (S (S Z))

          

          = S (plus (S Z) (S Z))

          

          = S (S (plus (S Z) Z))

          

          = S (S (S Z))


In the above trace, the second and third lines use the second rulein the definition of plus, and the fourth line uses the first rule.


1.2 Proofs

The ideas in this section are due to Hermann Grassman (1861),later taken up by Richard Dedekind and Giuseppe Peano.

How do we know our definition of plus corresponds to whatwe think of as addition? It’s hard to prove this without a formaldefinition of addition, and in fact our definition of plusis one of the best formal definitions of addition around. We can gainmore confidence by verifying that plus has the sameproperties that we expect of addition.

For example, we could try toverify the following claim, which intuitively says"adding a value and two is the same as applying S twice to that value".

Claim:For all Nats x,plus x (S (S Z)) = S (S x).

Note that this is a statement about an infinite number of Nats.But we want a finite proof, using only the rules that define plus.Here is one.


Proof:

          plus x (S (S Z))

          

           = S (plus x (S Z))

          

           = S (S (plus x Z))

          

           = S (S x)


That concludes the proof.

Why is this proof convincing?If we put in a specific value for x, say S Z,we get a legitimate computation trace. In fact, forthis particular choice, we get the trace displayed above.But any choice of value for x results in a legitimate trace.The body of the proof is not a specific computation trace, buta template for creating one, into which any specific value forx can be placed.

Here we have used a method of proving a “for all Natsx” claim that we might call the anonymous method,where we treat x as a complete unknown Nat (and thus "anonymous").If, while treating x in this way,we can prove some claim mentioning x, then it must be true nomatter what the value of x is, that is, it must be true forall Nats x.

Note that we have to agree that this is a legitimate method of proof,that is, it must be part of our proof system. The anonymous method is quitecommon in mathematics, though it’s usually not called that; in thestudy of formal logic, it is known as "for-all introduction".The anonymous method is limited in its application, however;it doesn’t always work to prove what we want.Consider the following claim, which intuitively says"Adding zero and any value produces that value".

Claim:For all Nats x,plus Z x = x.

A straightforward application of the anonymous method fails.If we consider plus Z x, we knowthat x is either Z or S y. In the firstcase, plus Z Z = Z, and the claim holds. But what aboutthe second case?


          plus Z (S y) = S (plus Z y)

          

                       = ?


We have no good way of proceeding.Let’s try some small cases to gain intuition.


          plus Z Z = Z

          

          plus Z (S Z) = S (plus Z Z)

          

                       = S Z

          

          plus Z (S (S Z)) = S (plus Z (S Z))

          

                           = S (S (plus Z Z))

          

                           = S (S Z)


Something interesting is happening here.The proof ofplus Z x = xfor x = S (S Z)contains a complete copyof the proof for x = S Z. You can see this by taking thelast three lines and stripping away the first expressionand then the outermost S (...) from each line.

If we consider the next case, the same phenomenon occurs.


          plus Z (S (S (S Z))) = S (plus Z (S (S Z)))

          

                               = S (S (plus Z (S Z)))

          

                               = S (S (S (plus Z Z)))

          

                               = S (S (S Z))


The proof ofplus Z x = xfor x = S (S (S Z))contains a complete copyof the proof for x = S (S Z).

More generally, we see thatfor any specific y,if we have a proof ofplus Z y = y, thenwe can construct a proof thatplus Z (S y) = (S y). Here’s how it goes:


          plus Z (S y)

          

            = S (plus Z y) [by the definition of plus]

          

            = S y          [because plus Z y = y]


Here we know we can get from the second line to the third linebecause we have a proof that plus Z y = y.

We have a proof of plus Z x = x for x = Z, andwe know that if we have a proof of plus Z x = x forx = y, where y is some Nat, then we can constructa proof of plus Z (S y) = (S y). These two statementsshould let us conclude that plus Z x = x is true for allNats x, because any Nat can be constructed by startingfrom Z and applying S some number of times.

We have discovered another way of proving afor all Nats x claim,which we will call the structural method.

To apply the structural method, we express the claim we wish to proveas a property P[x].For example, P[x]  =   "plus Z x = x".


To prove"For all Nats x, P[ x]" holds:
  1. Prove P[Z] holds.

  2. Prove that "For all Nats y,if P[y] then P[S y]" holds.


Intuitively, this makes sense. The two parts of the method mirror thetwo parts of the data definition of Nats. The first partshows that the claim holds for the simplest Nat, namelyZ, and the second part shows that when we construct a newNat from an old one, if the claim held for the oldNat, it holds for the new Nat. Since this is theonly way to construct a Nat, the claim must hold for allNats. However, we still have to accept this as a valid formof proof before using it.

Notice also that the second step asks us to prove yet another "for all"statement, and it looks more complicated than the one we set out to prove.However, it may not be. The new "for all" statement is of the form "If Q, then R".We prove such statements by assuming Q and using it to prove R. The hopeis that this may be simpler, because Q and R look similar in this case (they areP[y] and P[S y] respectively). So the assumptionthat Q holds might take us most of the way towards a proof that R holds.

For clarity, here’s a complete statement and proof of the previousclaim using the structural method.

Claim:For all Nats x,plus Z x = x.

Proof:By the structural method applied to x. Let P[x]  =   "plus Z x = x".

We first prove that P[Z] holds. This saysthat plus Z Z = Z. But this statement holds by the application ofthe first rule in the definition of plus to the left-hand side.

Next we prove "for all Nats y,if P[y] holds, then P[S y] holds".This says "for all Nats y, if plus Z y = y,then plus z (S y) = (S y)".We prove this by the anonymous method applied to y,which means we have to prove "if plus Z y = y,then plus z (S y) = (S y)".To prove this statement,we assume that "plus Z y = y" holds and use that to show that"plus z (S y) = (S y)" holds. The following argumentdoes this:


          plus Z (S y)

          

            = S (plus Z y) [by the definition of plus]

          

            = S y          [by the assumption of P[y]]


The structural method lets us conclude that "For all Natsx, P[x]" holds, as required. Thisconcludes the proof.

You will be asked to do proofs like this onassignments and exams, and marks will be deducted forpoor style, so study this example closely.

There are some stylistic points you should take note of here.The just-completed proof cites each method used and whatvariable it applies to (e.g. "the anonymous method applied to y").The proof also states the property P[x] explicitly, ratherthan leaving the reader to figure it out. There are many differentlogical statements used in the course of the proof, and the proof iscareful to specify them exactly and to be clear which one is beingworked on. When multi-line equational reasoning is used, the proofannotates each line with the reason why it is true, putting thereason in square brackets. Finally, the proof is also carefulto use fresh variables when needed. For example, ywas chosen because x was already in use. These touchesimprove clarity, and they should be part of your proofs as well.


1.3 Recursion

Sometimes structural recursion is not suited toa problem, or gives an inefficient solution, but when itworks, it is preferred.

Our definition of plus happens to be well-suited to proofsusing the structural method, because it uses a technique known asstructural recursion. Recursion is simply the use of thefunction being defined (plus in this example) on both theleft-hand side and the right-hand side of one or more rules in thedefinition. The recursion is structural because it mirrors thestructure of the data definition (in this case, the definition ofNat). This data definition is also recursive, since Nat appearson both the left-hand side and the right-hand side of at least one rule.

To see why plus uses structural recursion, let’s look at the definition ofNat. It has two parts: a Nat is either Z, or it is S appliedto a Nat. The definition of plus has two parts, correspondingto the two parts in the definition of Nat. The first part of thedefinition of Nat is that Z is a Nat. The first part ofthe definition of plus uses the pattern Z to see if the secondargument has the value Z. So the function mirrors the data definitionin this case.

The second part of the definition of Nat is that it is Sapplied to a Nat. The second part of the definition of plususes the pattern (S y) to see if the second argument is Sapplied to something, and gives that something the name "y". So far, thisis like the mirroring in the first case. But there is an additionalaspect of structural recursion here: because y is a Nat,structural recursion means that when plus is applied on the right-hand side,it is applied to y.

But of course plus has to be applied to two arguments, and the firstargument is also a Nat. But the pattern x in the definition ofplus just gives a name to the argument, which is used as is inthe application of plus on the right-hand side.This is another possibility in structural recursion.

This reasoning applies in general, not just to plus.Suppose we had a function mystery with one Natparameter. A definition of mystery that used structuralrecursion would look like this:


          mystery Z = ?

          

          mystery (S x) = ... mystery x ...


For a function enigma with two Nat parameters,there are more possibilities. Here is one:


          enigma x Z = ... x ...

          

          enigma x (S y) = ... enigma x y ...


Here, there is structural recursion on the second parameter, but notthe first. Our definition of plus looks like this possibility.


          plus x Z = x

          

          plus x (S y) = S (plus x y)


Here is a definition of multiplication that is also structurally recursive,using the same idea:


          times x Z = Z

          

          times x (S y)

          

            = plus x (times x y)


But when there are two Nat parameters, since structural recursion permitseach argument to be either unchanged or handled according to the recursivedefinition of Nat, there are other possibilities.We could have structural recursion on the first parameter, but notthe second:


          enigma Z y = ... y ...

          

          enigma (S x) y = ... enigma x y ...


And we could have structural recursion on both parameters:


          enigma x Z = ... x ...

          

          enigma Z y  = ... y ...

          

          enigma (S x) (S y) = ... enigma x y ...

                               ... enigma (S x) y ...

                               ... enigma x (S y)


Not all three of those possibilities on the last right-hand side mightbe necessary. The point is that the recursive applications ofenigma have as arguments either the original unchangedarguments on the left-hand side or the values named in the structuralpattern on the left-hand side.

This is perhaps made more clear by an example of recursion that is not structural.Here is a non-structural alternate definition of addition.


          add x Z = x

          

          add x (S y)  = add (S x) y


This fails to be structural recursion because the first parameter xis not treated in a structural fashion. It is not unchanged on the right-handside, but it is not treated according to the definition of Nat, likethe second parameter is.

Here’s an example of a computation using this new definition.


          add (S Z) (S (S Z))

          

            = add (S (S Z)) (S Z)

          

            = add (S (S (S Z))) Z

          

            = S (S (S Z))


The definition of add looks perfectly reasonable, and itseems to work. Why should we preferplus to add? The difficulty withadd is that it is harder to prove things about it thanit is for plus.Suppose, as before, we try to prove that for all Natsx, add Z x = x.


          add Z Z = Z

          

          add Z (S y)

          

            = add (S Z) y

          

            = ?


It’s not clear how to proceed. In fact, it is possible to prove the claimusing the structural method, but it takes more work.

Here is another example of non-structural recursion, a definition ofthe function idiv2 implementinginteger division by 2, rounded down (so the result of applyingidiv2 to the representation of 5 would be therepresentation of 2).


          idiv2 Z         = Z

          

          idiv2 (S Z)     = Z

          

          idiv2 (S (S y)) = S (idiv2 y)


Again, this seems like a reasonable definition, and it appears to work(it is, in fact, correct). This is not structural recursion because thereis an extra case (the second one) in the definition for when the argument is ourrepresentation of one, and because the third case uses the pattern (S (S y))instead of (S y).

Programming assignment submissions in this course are machine-tested,and so submissions that are correct but do not use structural recursion willearn marks. However, marks will be deducted when hand-marking is used,such as on exams. Since developing correct structurally recursive code isusually easier, you should get into the habit of trying it.

Sometimes, non-structural recursion is necessary. But wheneverpossible, we should use structural recursion. Not only is it harder toformally prove properties of non-structurally-recursive code, it isalso harder to informally reason about it, meaning that such code ismore likely to contain programming errors. For that reason, assignmentsin this course will often require you to use structural recursion (we willdiscuss later how to make sure that you are using structural recursionin Racket).

We will see other recursive definitions soon, and the reasoning weused above with respect to determining whether a function definition isstructurally recursive will apply to those definitions as well.

The structural method for proving properties of programs that performcomputation on Nats has at its core the requirement to provea statement of the form "For all Nats y,if P[y] holds, then P[S y] holds."To prove an "if-then" statement, we assume the "if" part, andtry to reason that the "then" part holds. When developing codeusing structural recursion, it helps to keep this in mind, even ifyou don’t ever prove anything about the code. Structural recursionmeans that the form of any recursive application of the functionyou are writing is limited to a few possibilities. Just as thestructural method lets you assume that the property holds for the"smaller" instance P[y], in developing code using structuralrecursion, you should assume that the recursive applications work asdesired. Just as the structural method has you try to reason that the"then" part holds, in developing the code, you should try to figure outhow the (presumed correct) result of the recursive application can be usedto figure out the result for the original argument.


1.4 The standard representation

As briefly mentioned above, Haskell has its own representation ofintegers (which include all the natural numbers), with familiarnotation (such as infix +).But we can transfer the idea of structural recursion forcomputation with Nats over to computation on naturalnumbers using Haskell’s representation.

You may be used to having the naturalnumbers start at 1, but starting at 0 makes more sense,and this is common in computer science and formal mathematics.

A Nat is either Zor S applied to a Nat.By analogy, a natural number is either 0 or n+1 , where n is a natural number.Here is an example of structural recursion on natural numbers,converting Haskell’s representation to ours.


          toNat 0   = Z

          

          toNat (n+1) = S (toNat n)


Haskell’s built-in representation of natural numbers, unlike our definitionof Nat, can handle negative numbers as well; that is, it is reallya representation of integers. (We will discuss how to extend Nat tohandle negative numbers later in the course.) In mathematics, we representthe natural numbers by N and the integers by Z .toNat is a function which consumes a natural number and producesa Nat, so mathematically it would be described as toNat:NNat . In Haskell,it is described using the following syntax:


          toNat :: Integer -> Nat


This description is known as a type signature, but we willoften call it a contract.

The conversion in the other direction uses structural recursion on Nats.


          fromNat :: Nat -> Integer

          

          fromNat Z = 0

          

          fromNat (S x) = 1 + fromNat x


What is the contract for add? It consumes two Natsand produces a Nat. Mathematically, we would describe itas add:N×NN .Here is the Haskell contract:


          add :: Nat -> Nat -> Nat


An arrow is placed between each argument as well. There is a technical reasonfor this, which we will discover in lecture module 04.


The anonymous method and the structural methodwork for natural numbers.To prove"For all natural numbers m , P[ m ]" holds:
  1. Prove P[0] holds.

  2. Prove that "For all natural numbers n ,if P[ n ] then P[ n+1 ]" holds.


This is also known asmathematical induction. It is a central proof technique inmathematics, and is exercised extensively in Math 135/145.In fact, what we have called the structural method is usuallycalled structural induction.

From a mathematical point of view, our definition of Natand associated computations is perfect. In fact, it is the standarddefinition of natural numbers used in formal logic and the foundationsof mathematics. But it is not a perfectly satisfactory definition from a computerscience point of view. A full explanation needs to be deferred untilmuch later (perhaps CS 146), but we can get an intuitive idea nowof why it is not a perfectly satisfactory definition.Consider the following computation.


          m = toNat 1000000

          

          times m m = ?


Think about what m would look like printed out. It wouldtake up a lot of space, much more space than the familiar number1000000. This is an indication of an inefficient use of space.We will not talk much about use of space until CS 146,but it will hover in the background during CS 145.

Now think about how much time the multiplication would take,compared to the pencil-and-paper multiplication of 1000000 times1000000. This is an indication of an inefficient use of time.Our representation and our algorithms for computation using that representation areinefficient. We will see, later in the course, how to be muchmore efficient, using tools and techniques we’ve already discussed.


1.5 Tuples

In the remainder of this lecture module, we’ll take a very quick tourthrough some other features of Haskell, as foreshadowing for morecareful study of the ideas shortly.

Each parameter in a function is one value, and the result of thefunction is also one value. We may wish to group two or more valuesfor conceptual or computational purposes. For example, themathematical notation (4,3) might represent a vector intwo-dimensional space, and we might wish to write a function thatscales such vectors. Scaling our example by a factor of 2 gives thevector (8,6) . We can do this grouping with the following Haskell datadefinition and function definition:


          data Pair = P Integer Integer

          

          scale :: Integer -> Pair -> Pair

          

          scale s (P x y) =  P (s*x) (s*y)


An example of computation:


          scale 2 (P 4 3) = P 8 6


We can generalize the concept to not just pairs (or 2-tuples) ofintegers, but pairs containing two different data types.


          data Pair t1 t2 = P t1 t2


Here t1 and t2 are Haskell type variables, that is,they represent unknown types. The same definition of scalewill still work, but now it works not only on pairs of integers, buton pairs containing any types for which the * operation isdefined.

The notation P 4 3 is not as readable to mathematicians as(4,3), and it should not surprise you that the latter (mathematical)tuple notation is built into Haskell. Here is a redefinition ofscale and a sample computation using built-in tuples.


          scale :: Integer -> (Integer, Integer)

                           -> (Integer, Integer)

          

          scale s (x,y) = (s*x, s*y)

          

          scale 2 (4,3) = (8,6)



1.6 Sequences

The function scale worked only with pairs or 2-tuples. Wecan use the same idea to create tuples of any length, but we wouldhave to write separate functions for each length. How can wemanipulate sequences of arbitrary length, such as 4, 3, 5, 1, 4, 2?

We combine the ideas for Nat and Pair, andcall the result List. With Nat, we had aspecial data constructor Z for zero. The correspondingdata constructor for List is E, representingthe list (sequence) of length zero, or the empty list.

Nat had the data constructor S to add one toan existing Nat. List has the data constructorC to add one number to an existingList. Better yet, we can use a type variable, so that wecan form not only Lists of numbers, but Lists of arbitrary types.


          data List t = E | C t (List t)


The list containing only 2 is represented by C 2 E. Thelist 4, 2 is represented by
C 4 (C 2 E).

Just as our definition of Nat was recursive, ourdefinition of List is recursive. A List of type tis eitherE or it is C applied to an element of type tand a List of type t.

Since we have a recursive definition for List, wecan speak of structural recursion on Lists.As an example, we can generalize our earlierdefinition of scale to scale the values in a List,if it contains integers.Scaling the list C 4 (C 2 E) by a factor of 3 shouldresult in C 12 (C 6 E). Here’s the code, which isstructurally recursive:


          scale :: Integer -> List Integer -> List Integer

          

          scale x E = E

          

          scale x (C y ys) =  C (x*y) (scale x ys)


And here’s a sample computation:


          scale 2 (C 4 (C 3 (C 5 E)))

          

          = C 8 (scale 2 (C 3 (C 5 E)))

          

          = C 8 (C 6 (scale 2 (C 5 E)))

          

          = C 8 (C 6 (C 10 (scale 2 E)))

          

          = C 8 (C 6 (C 10 E))


Here’s structurally recursive code to add up the numbers in a List:


          sumList :: List Integer -> Integer

          

          sumList E = 0

          

          sumList (C x xs) =  x + sumList xs


And a sample computation:


          sumList (C 4 (C 3 (C 5 E)))

          

          = 4 + sumList (C 3 (C 5 E))

          

          = 4 + 3 + sumlist (C 5 E)

          

          = 4 + 3 + 5 + sumList E

          

          = 4 + 3 + 5 + 0

          

          = 12


We’ve seen a function that consumes a List and produces aList of the same length (scale), and afunction that consumes a List and produces anInteger. Here’s a function that consumes aList and produces a List of a differentlength. The idea is to produce a list with all occurrences of a givenvalue removed from a given list. For example, to produce the listresulting from removing the value4 from the list 4,3,4, we would use the expressionremove 4 (C 4 (C 3 (C 4 E))). The result should be
C 3 E, representing the list containing only 3.

We use the structural recursion template to write the code.


          remove :: Integer -> List Integer -> List Integer

          

          remove x E = E

          

          remove x (C y ys) = ?


What to replace the ? with depends on the values ofx and y. If they are equal, then the valuey should not appear in the result, otherwise itshould. This is expressed in Haskell as follows:


          remove x E = E

          

          remove x (C y ys) | (x == y)  = remove x ys

                            | otherwise = C y (remove x ys)


Note the use of == for the equality test. Here is a samplecomputation.


          remove 4 (C 4 (C 3 (C 4 E)))

          

          = remove 4 (C 3 (C 4 E)))

          

          = C 3 (remove 4 (C 4 E))

          

          = C 3 (remove 4 E)

          

          = C 3 E


Once again, Haskell has built-in, more convenient notation for lists, justas it does for tuples. The emptylist is represented by [], and the C dataconstructor (called cons in both Haskell and Racket) is the infixoperator :. Here is the empty list, the list containingjust 4, and the list 4,3,5.


          []

          

          4 : []

          

          4 : (3 : (5 : []))


In Haskell, the : operator associates to the right, so wecan take the brackets off the last line. But there is even moreconcise notation to represent these values:[4] and [4,3,5] for the last two lines.

Here is sumList written using the new notation:


          sumList :: [Integer] -> Integer

          

          sumList [] = 0

          

          sumList (x:xs) =  x + sumList xs


Notice the new pattern (x:xs). It matches a nonempty list,giving the name "x" to the first element of the list, and the name"xs" (think: "the rest of the x’s") to the rest of the list.

There are similar mechanisms in Racket, which is not as concise andexpressive as Haskell, but easier to program in for the beginner, and easier toexplain rigourously. We will learn about Racket in the next lecture module.

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值