some posts
Getty Ritter
9 years ago
1 | Years ago, I worked at the [Berkeley Self-Paced Center], which is a | |
2 | wonderful institution. It was designed to be a self-directed, | |
3 | one-on-one center where people could go to learn a new programming | |
4 | language (or sometimes programming environment) in a structured way | |
5 | but at the student's own pace[^1]. | |
6 | ||
7 | [^1]: At least, nominally; in practice, the large influx of students | |
8 | at the end of the semester—who had forgotten about their enrollment | |
9 | until on the verge of failing—led us to experiment with various | |
10 | ways of imposing a loose schedule on students, so that | |
11 | we could try to even out the frequency of student visits. | |
12 | ||
13 | One of the early assignments in these courses I really liked, | |
14 | because it was (I felt) a program that was not particularly complicated | |
15 | or large, but at the same time conveyed several useful, | |
16 | _practical_ concepts that don't show up in Hello World or Project Euler | |
17 | examples. | |
18 | ||
19 | The example was a simple MadLibs program. MadLibs is a simple game in | |
20 | which one is presented first with a series of blanks accompanied by | |
21 | requests for particular classes of words—sometimes parts of speech | |
22 | such as nouns or verbs, sometimes more specific descriptors such as | |
23 | food items or animals—and then those are stitched into a sentence, | |
24 | where they produce humorously nonsensical phrases.[^2] | |
25 | ||
26 | The program in question required that the MadLib be chosen randomly | |
27 | from a list of possible ones, and that the blanks be requested in | |
28 | a random order which did not necessarily correspond to their ordering | |
29 | in the template phrase. | |
30 | ||
31 | A program to implement this would necessarily involve several tasks | |
32 | in the relevant language: input _and_ output from a user, randomness, | |
33 | some kind of data structure for keeping the intermediate values, and | |
34 | possibly some kind of string processing. If one adds a small extra | |
35 | stipulation—that the MadLibs templates must be read from a file—then | |
36 | it also involves file IO, making it a good whirlwind tour of a | |
37 | language's features in practice. | |
38 | ||
39 | What I'd like to do is use this MadLibs program as a way of exploring | |
40 | other programming languages, and in particular, niche, emerging, or | |
41 | forgotten languages which people might not be widely familiar with. | |
42 | ||
43 | ## The MadLibs Program | |
44 | ||
45 | The rough steps which will be performed in most versions of the | |
46 | program are as follows: | |
47 | ||
48 | 1. Open a file named `template.mad`. This file contains one or more | |
49 | lines that have sentences with certain parts inside matching | |
50 | curly braces, e.g. | |
51 | ||
52 | > This {noun} will {verb}! | |
53 | ||
54 | After this file is read in, one of the lines is selected at random | |
55 | to be the template used. | |
56 | ||
57 | 2. Split that template into two sequences: the literal text segments | |
58 | and the part-of-speech segments, as in | |
59 | ||
60 | > ["This", "will", "!"] | |
61 | > [ "noun", "verb" ] | |
62 | ||
63 | 3. Print out requests for the part-of-speech segments, read in | |
64 | lines from the user, and fill them into the template. Do this in | |
65 | random order but still filling in the correct gap. | |
66 | ||
67 | > Give me a verb: jump | |
68 | > Give me a noun: dog | |
69 | ||
70 | 4. Print the resulting sentence | |
71 | ||
72 | > This dog will jump! | |
73 | ||
74 | [^2]: I played this a lot as a child, in which most of the words | |
75 | chosen were scatological or otherwise puerile. Which is to say: | |
76 | most of the blanks were filled in with some variation on "poop". |
1 | For the first MadLibs examples, let's try writing some Lisps. People | |
2 | soetimes talk about 'Lisp' as though it were some specific language, | |
3 | but Lisp is a family of related languages with some commonalities. | |
4 | Broadly speaking, Lisps are dynamically typed, have functional | |
5 | features like closures but aren't necessarily pure, and use a syntax | |
6 | based on S-expressions. These aren't always the case—Shen is a | |
7 | statically typed Lisp, Common Lisp has functional features but is | |
8 | often used imperatively, and Dylan is a Lisp that uses a more | |
9 | traditional Algol-like syntax—but they are broadly applicable. | |
10 | ||
11 | The S-expression syntax is often a sticking point for people first | |
12 | learning Lisp, but it's also one of the powerful features of | |
13 | Lisp. In a typical Algol-descended language like (say) C, a | |
14 | compiler will start by taking your textual source and parsing | |
15 | it into a kind of tree. An expression like `f(3 + x * 2)` has | |
16 | to be transformed int oa hierarchical representation that the | |
17 | computer can understand, applying order-of-operations so | |
18 | that it's clear how to group the relevant operations. | |
19 | ||
20 | ~~~~ | |
21 | | | |
22 | +--+---+ | |
23 | | | | |
24 | | +--+--+ | |
25 | | | | | |
26 | | | +-+-+ | |
27 | | | | | | |
28 | f ( 3 + x * 2 ) | |
29 | ~~~~ | |
30 | ||
31 | In typical Lisp syntax, this tree is written more-or-less | |
32 | explicitly, using parentheses to indicate the relevant grouping, | |
33 | as `(f (+ 3 (* x 2)))`. This means that arithmetic expressions | |
34 | can be a bit syntactically noisy, but has the benefit of being | |
35 | very clear and uniform. Even higher-level syntactic constructs | |
36 | use this format: where C would use curly braces or some other | |
37 | syntactic construct, Lisps continue to use parentheses. For example, | |
38 | the C program | |
39 | ||
40 | ~~~{.c} | |
41 | int max(int x, int y) | |
42 | { | |
43 | if (x > y) { | |
44 | return x; | |
45 | } else { | |
46 | return y; | |
47 | } | |
48 | } | |
49 | ~~~ | |
50 | ||
51 | is analogous to the Scheme program | |
52 | ||
53 | ~~~~{.scheme} | |
54 | (define (max x y) | |
55 | (if (> x y) x y)) | |
56 | ~~~~ | |
57 | ||
58 | This makes |
1 | PicoML | |
2 | ====== | |
3 | ||
4 | PicoML is an ML- and Haskell-inspired language that is designed to be | |
5 | easy to implement, straightforward, consistent, and small. | |
6 | ||
7 | ~~~~ | |
8 | fact : Int -> Int | |
9 | 0 = 1 | |
10 | n = n * fact (n - 1) | |
11 | ||
12 | main () = putln ("fact 5 = " <> fact 5) | |
13 | ~~~~ | |
14 | ||
15 | Type declarations are done either with the `data` or the `record` keywords: | |
16 | ||
17 | ~~~~ | |
18 | data List a = | |
19 | [ Nil | |
20 | , Cons a (List a) | |
21 | ] | |
22 | ||
23 | record Stream a = | |
24 | { .head a | |
25 | , .tail (Stream a) | |
26 | } | |
27 | ~~~~ | |
28 | ||
29 | Types created with `data` are always eager and types created with `record` | |
30 | are always lazy, so you can easily create infinite streams: | |
31 | ||
32 | ~~~~ | |
33 | streamZip : (a -> b -> c) -> Stream a -> Stream b -> Stream c | |
34 | streamZip f s1 s2 = | |
35 | { .head = f s1.head s2.head | |
36 | , .tail = streamZip f s1.tail s2.tail | |
37 | } | |
38 | ||
39 | fibs : Stream Int = | |
40 | { .head = 1 | |
41 | , .tail = streamZip _+_ fibs fibs.tail | |
42 | } | |
43 | ~~~~ | |
44 | ||
45 | Recursive bindings are allowed in a type-directed way, so a binding whose | |
46 | type is a function or a record will allow recursive bindings, but a binding | |
47 | whose type is data will not allow a recursive binding. | |
48 | ||
49 | ~~~~ | |
50 | listZip : (a -> b -> c) -> List a -> List b -> List c | |
51 | f (Cons x xs) (Cons y ys) = Cons (f x y) (listZip f xs ys) | |
52 | _ _ _ = Nil | |
53 | ||
54 | tail : List a -> List a | |
55 | tail (Cons x xs) = xs | |
56 | tail Nil = error "tail of empty list" | |
57 | ||
58 | -- this definition will fail, because one cannot recursively refer to | |
59 | -- value bindings | |
60 | badFibs : List Int | |
61 | = Cons 1 (zip _+_ badFibs (tail badFibs)) | |
62 | ~~~~ | |
63 | ||
64 | The language is not pure, but all bindings are immutable, so reference cells | |
65 | are provided in the stdlib: | |
66 | ||
67 | ~~~~ | |
68 | impFact : Int -> Int | |
69 | n = let acc = ref 0 | |
70 | ; let num = ref n | |
71 | ; while { !num > 0 } | |
72 | { acc := !acc * !num | |
73 | ; num := !num - 1 | |
74 | } | |
75 | ; !acc | |
76 | ~~~~ | |
77 | ||
78 | This also demonstrates the other usage of curly braces: they are sugar for | |
79 | an anonymous function that takes a `()` argument, i.e. the above `impFact` | |
80 | definition is equivalent to | |
81 | ||
82 | ~~~~ | |
83 | impFact : Int -> Int | |
84 | n = let acc = ref 0 | |
85 | ; let num = ref n | |
86 | ; while (() -> !num > 0) (() -> | |
87 | ( () -> | |
88 | acc := !acc * !num | |
89 | ; num := !num - 1 | |
90 | ) | |
91 | ; !acc | |
92 | ~~~~ | |
93 | ||
94 | This means that `while` is a function included in the stdlib, as well: | |
95 | ||
96 | ~~~~ | |
97 | while : Thunk Bool -> Thunk () -> () | |
98 | cond body = if | cond () -> (body () ; while cond body) | |
99 | | else -> () | |
100 | ~~~~ | |
101 | ||
102 | Finally, PicoML has a mechanism for supplying values implicitly. The | |
103 | `implicit` keyword can be used on value bindings and on function | |
104 | arguments. The language will then infer, based on the type, whether or | |
105 | not the user supplied the argument, or whether it needs to reach for | |
106 | the implicit argument: | |
107 | ||
108 | ~~~ | |
109 | implicit n : Int = 5 | |
110 | ||
111 | shout : implicit Int -> () | |
112 | x = println (show x) | |
113 | ||
114 | main = | |
115 | { shout 8 -- prints 8 | |
116 | ; shout -- prints 5 | |
117 | } | |
118 | ~~~ | |
119 | ||
120 | If there are _multiple_ implicit values in scope whose types match, | |
121 | then PicoML will raise a compile-time error: | |
122 | ||
123 | ~~~~ | |
124 | implicit n : Int = 5 | |
125 | implicit m : Int = 6 | |
126 | ||
127 | main = { shout } -- will not compile, because of the ambiguity | |
128 | -- as to which implicit to choose | |
129 | ~~~~ | |
130 | ||
131 | This, combined with manual dictionary-passing, is how typeclass-like | |
132 | functionality is implemented in PicoML | |
133 | ||
134 | ~~~~ | |
135 | record Monoid m = | |
136 | { .mempty m | |
137 | , .mappend (m -> m -> m) | |
138 | } | |
139 | ||
140 | implicit additiveMonoid : Monoid Int = | |
141 | { .mempty = 0 | |
142 | , .mappend = _+_ | |
143 | } | |
144 | ||
145 | multiplicativeMonoid : Monoid Int = | |
146 | { .mempty = 1 | |
147 | , .mappend = _*_ | |
148 | } | |
149 | ||
150 | mconcat : implicit (Monoid a) -> List a -> a | |
151 | m (Cons x xs) = m.mappend x (mconcat m xs) | |
152 | m Nil = m.mempty | |
153 | ||
154 | main = | |
155 | { print (mconcat [2,3,4]) -- prints 9 | |
156 | ; print (mconcat multiplicativeMonoid [2,3,4]) -- prints 24 | |
157 | } | |
158 | ~~~~ |