some posts
Getty Ritter
10 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 | ~~~~ |