[mercury-users] Comments & feedback on Ralph Becket's tutorial (long)
Ron Newcomb
pscion at yahoo.com
Fri May 14 13:43:15 AEST 2010
Hi Mr. Becket, (and other Mercury users)
I recently read your Mercury tutorial at
http://www.mercury.csse.unimelb.edu.au/information/documentation.html
and, as you had requested feedback on the fledgling document, and I am new to logic programming, I am duly replying with opinions and such on said document.
A quick bio of me, not because I'm all that interesting, but just so you know who this audience of one is. I learned Commodore Basic when I was 10, learned the Commodore's assembly language at 15, got an IBM x86 in high school and started writing videogames in C / x86 assembly, and finally majored in Comp Sci with the requisite half-semester exposure to Prolog. Though I'm very skilled at plain ol' C, I didn't do very well with Prolog. But I'm still intrigued by the ideas in logic programming, and still wish to learn it. As Prolog isn't useful for anything sizeable and has too many dialects for me to trust writing anything in it, Mercury is my chance.
I also recently wrote the Programmer's Guide to Inform 7, the latter being a rule-based imperative language with a heavily natural language syntax. (The manual for it is intended for writers and laymen with little exposure to programming.) So I know a little about writing manuals for programmers who are new to a paradigm, especially regarding convincing typical OO programmers that the weird new language they are looking at is not unnecessarily verbose or with clumsy, ugly syntax.
My words below are in places a bit terse and note-like, and you'll likely need a copy of your document open alongside to follow me.
So here goes.
---------------------
First of all, it is a truth universally acknowledged that a prospective programming language must never appear to be unnecessarily verbose or clumsy in its syntax. If a prospective user senses that many tasks would be difficult or require lots of boilerplate typing, he'll not invest his time and search elsewhere. On this point I will hit upon more than once.
I cannot copy-paste examples from the PDF into a text editor. Trying to do so inserts illegal characters into the text file which the compiler can't handle, and which my text editor (TextEdit, Mac OS X) can neither see nor remove. While I have much nostalgia for the halcyon days of pecking a program into a computer from an issue of a computing magazine, life is short.
(Postscript seems only to exist in academia; I can't comment on its appropriateness.)
These three lines:
:- module foo.
:- interface.
:- implementation.
...represent well-known concepts from software engineering. Show and explain these first, together, so the reader can immediately see the broadest structure of a Mercury source file. This will likely appear comforting to those used to OOP ideas of encapsulation and abstraction. If the module name needs to match the filename, mention it.
:- import_module io.
Then mention that import_module line(s) can appear in either the interface or implementation section. This is important because the compiler cares which one it's in, and will complain otherwise.
With the boilerplate out of the way we can finally get to the program itself.
:- pred main(io::di, io::uo) is det.
Many languages call this a "prototype" or "signature" or somesuch to describe such information. Please use that terminology. Also call out the module name "io" in front of the :: operator so more of it appears familiar to the typical software engineer.
For main() itself, use the !IO syntax and explain that the ! means there's a pair of variables, one input and one output, so it actually does match the prototype. (And explain that you'll explain them in more detail later.) For beginners, it is highly important to simply, to omit needless words, and this frequently means generous helpings of syntactic sugar.
main(!IO) :- io.write string("Hello, World!\n", !IO).
You had written:
"Finally, we have a clause defining main. A clause comprises a head and body separated by a :- symbol. The head of this clause tells us that this is a definition for main and names its two arguments IOState in and IOState out. The body of the clause, which is executed when main is called, consists of a single goal calling io.write string (i.e., the write string predicate defined in the io module) with a message (Mercury interprets the \n sequence in the string argument as a literal newline character) and the two IOState arguments."
Holy expository lump, Batman! Most if not all of this paragraph is completely unnecessary. While "head" and "body" might prove useful soon, what you previously called a "predicate" you are now calling a "clause". Rather, make the point that what many languages call a "procedure" or "subroutine" we call a "predicate". Meaning, a function returns a value while a predicate does not. (Even if it's a bit misleading to say that. We can be pedantic later.) Most of those vocabulary terms are unnecessary, and confuse the task of teaching the language with teaching the culture that usually uses it. No one calls a print statement a "goal", for example.
"Program variables in Mercury always start with a capital letter or underscore, while names of predicates, types, modules and so forth do not."
Though it goes without saying that Mercury is a case-sensitive language, it should be said. And with the above sentence following directly thereafter. Then the additional point needs be made about how Mercury is precisely backward vis-a-vis English. In English, unique things are capitalized: King Ferdinand, the Space Needle, the Great Exodus, while "variables" are lowercase: the king, the monument, the historic event. If it's a hurdle to the programmer (as some of us are more sensitive to such things than others) emphasize that the underscore, resembling a named blank, can always be used to make the program look like one of those ad-lib word puzzles, __like_this__.
The rest of the chapter on the nitpicky details of IO should be saved for much later. But when referring to !IO and its messy prototype, it is important to explain it as "we'll see what we can do with that, later". Without such a suggestion of undiscovered feature, the !IO will appear as needlessly verbose syntax whose only job is to appease a compiler, and moreover, a compiler with a misplaced sense of aesthetics.
You skipped over what "is det" means in the prototype, which I think that's OK for now, but remove it from the summary since you haven't introduced any alternatives to det.
-----------
I recommend just using the function form of Fibonacci (not the one-liner version of it, though; not at first). It's less verbose than the predicate -- which is using those comma operators you haven't explained yet -- and the audience will be more comfortable with functions than with predicates anyway regardless whether they came from LISP or C. (It may be unfortunate that main isn't itself a function.)
I recommend introducing switch before if-then-else for several reasons. One, the else part is mandatory. This restriction will bother a great many [imperative] people, and they'll assume the syntax, and hence the language as a whole, is clumsy. Two, switch can check that all cases are considered precisely once, so it's safer. Three, switch is optimized better by the compiler, using lookup tables or hashes or whatnot -- C/C++ people will appreciate that point very much, and may even jibe the Java people with "even Mercury is faster than Java". Finally, four, it allows introduction of the differences between det and semidet (which you did in the rot13 example) in the guise of controlling what compiler errors one wants or doesn't want.
The switch is also a good place to introduce the , comma and ; semicolon operators as logical-AND and logical-OR, respectively. Liken the switch to a C if-condition that purposefully uses short-circuit evaluation. (A brief reminder of what short-circuit evaluation is is probably called for, but do so in the context of an imperative language.) Since the whole switch is inside the parenthesis, again like a C condition, it's a strong analogy.
(Incidentally, when if-then-else finally is introduced, introduce its expression form because it has a direct counterpart in C in the ternary ?: operator: return (n >= 2) ? 1 : (fib(n-1) + fib(n-2)); Many other languages have something similar precisely because C had it and it was appreciated there. Mention C, mention them, and then mention your point about how this is the usual form used in Mercury. With that setup, the point about the else branch being mandatory will not feel like Mercury has clumsy syntax, but rather, that it is falling into line with many other popular languages.)
You mention unification about here, but never actually define it or distinguish it from assignment as far as I can tell. That's probably at least a page's worth of explanation right there. (My budding impression of unification goes like this: every variable has a known/unknown property. The "unify" operation is either 1) a compare operation or 2) an assignment operation, depending upon if the two variable's values are 1) both known or 2) one of each.) Unfortunately I can't come up with any examples where, during imperative coding, I had thought, "You know, a unify() ability would really be handy here."
The summary mentions that a bodyless clause must omit the :-, but I think that point goes into the next chapter.
-----
The example on input uses a variable called String. That's a poor choice of name. "String" is a reserved word in many languages, so ok(String) looks like a typecast to type "string" rather than assignment to a variable called String. Instead, use "MyString". Moreover, it's unclear what ok() does, especially because the meat of the condition -- the part likely to fail -- is the to_int() call. "Data constructor" isn't defined. I am assuming ok() tests for end-of-input, like the eof() C function, but I'm unsure. Most languages have the read() function itself return any EOI error or message, rather than a second function to "go get" that information. So, again, Mercury is looking verbose and clumsy. You do tack on an also-ran paragraph in which you then call ok() a "deconstruction", again without defining the term. That paragraph tells me nothing that connects to anything I know.
By contrast, i(), f(), c(), and s() *do* seem to be typecasts. Or at least type-assertions that will fail & backtrack.
Don't call if-then-else a "goal". An if-statment, like any other construct, is a means to an end, not the end in itself. I realize such vocabulary is popular around logic languages, but it's part of the wall preventing wider adoption of LP.
(Recursion is another part of that wall, but you probably knew that. That code looks unstructured to my eye. In the input example, and I believe in many tail-recursive situations where the parameters don't differ, it essentially acts as a GOTO. Much better would be if, as syntactic sugar, Mercury provided a standard "next" or "continue" or "retry" or "redo" or some such. Incidentally, perl uses "next" to re-start a loop. By using the same, Mercury would quietly imply that every function/predicate/clause is potentially its own loop, thereby implying recursion without actually saying so. Granted, the analogy breaks if it's not tail-recursion.)
"A disjunction is a sequence of goals separated by semicolons."
Just say "The semicolon is the logical-OR operator, analogous to C's || or <another example>." Furthermore, it would be good to point out that the comma is the logical-AND (in C, the && operator), and then those parenthesis begin to make sense. Finally, recall "short-circuit evaluation", and the flow of a body all falls into place -- including failure and backtracking as an if-condition returning false. 'The individual cases of a switch are just the ordinary logical-OR operation followed by a normal if-condition. If the condition fails, short-circuit evaluation demands control execution jumps forward to the next semicolon, or the whole thing returns failure.'
-------
rot13 is a horrible example. Nothing makes logic languages look more verbose, more clumsy, than an example imploring the programmer to write out a similar statement 52 times, where in an imperative language a couple of branches and arithmetic would solve it quickly. Strike this example completely.
(The telephone# example is a little better, but usually reams of data are not hardcoded into a program.)
While the point that a disjunction can be written as separate clauses is interesting, stick it somewhere where it would be appreciated, such as a long, complex function. (A long example of input that must look for eof and other errors might even be a decent example, because it extracts the drudgework of checking for edge cases from the real purpose of the function. Such modularization of error state from garden path would be appreciated by OOP programmers and anyone who likes their own language's exception-handling mechanisms.)
-----------
Cryptarithms, while a fine example of something LP is good at, are difficult for the beginner to follow program flow. I couldn't solve that in C without examining it heavily, let alone in an unfamiliar language. The beginner needs problems he already knows the solution to, because the methodology of solving it is itself the problem to be puzzled out. Asking a beginner to puzzle out how to solve the example's problem at the same time as using an unknown methodology asks too much.
Save this example for later, for when the beginner is very familiar with syntax, backtracking, etc.
--------
"CHAPTER 2: THE MERCURY TYPE SYSTEM" No one cares. Perhaps, "COMMON TYPES OF VALUES" or "VALUES READY TO USE" or other wording implying the chapter holds useful features, rather than an arcane subject which must be learned.
Tuples aren't needed.
Lists are a little late since we already used them without introducing them. Might want to refactor that earlier example not to use them? I think they could wait a little longer, perhaps.
The "discriminated union types" -- what a mouthful! -- are a great feature to introduce this early. They allow the beginner to begin coding something that means something personal to them: colors, clothing, the characters from Star Wars, whatever. Allowing them to play with the language rather than following the examples verbatim encourages language buy-in. (Also, consider moving Equivalence Types up here. Not the parameterized versions, but it is important to explain why one uses the == operator and the other uses the ---> operator even those both are :- type. I'm assuming the two features are very closely related anyway.)
The cards are also a good example. (And look, there's that semicolon again, still meaning "or".) But, a problem I had:
:- type determiner ---> a; an; the; some.
:- type is_are ---> am; is; are.
... break the compiler. Warn what happens when reserved words are used in a Discriminated Union Type. Anyway, switches on these would be a good time to introduce det versus semidet versus whatever other modes you can shoehorn into there. Just watch out for those reserved words, as I immediately slammed into.
Before introducing the bank account examples, let's explore more with the Discriminated Union Types (hereafter referred to as "duts"). Once the beginner playfully writes his own, show how to parse and then print out duts. And then show how to do it within a simple grammar, such as the programmer would enter "number, dut, string", and the program would pick it apart and reprint it in a different order.
Now the bank account example of structs / records: the bank account example explains too much. There's a difference between showing how to use BankAcct^funds, and showing how its implemented behind the scenes. Save the latter for later. Otherwise the explanation and the caveats are pretty good for a first draft.
Then perhaps an example with parsing and output using the valued properties within a bank account. This allows the beginner to code his own tiny world of objects, properties, and values, and input queries to them. And output answers, completing the positive feedback loop.
Now I would perhaps pull the phone# example for the reason of showing multiple modes of a predicate. This is where the beginner really steps into the deeper end of the logic programming pool: a basic feature still little-known outside the LP paradigm. While explaining the modes and such, keep in mind the programmer must leave the tutorial periodically to go play with his own toy program, trying something out and getting it to compile. The phone# example, because it uses multiple modes on a single predicate to "fill in the missing parameter; what value goes with these passed-in values?", tie this into whatever example the beginner just wrote using and parsing his duts. If the beginner can get a simple parsable input going, get it to query the duts and structs and properties he made into a mini-database ("WHO-IS BROTHER TO LEIA?"), and print it out again, then we, and the beginner, have accomplished our first major milestone.
---------
I'll stop here. I hope I haven't caused offense; I really appreciate the tutorial, in any state of writing. I will also take the liberty of posting to comp.lang.prolog, which seems appropriate, and would perhaps generate discussion.
Thank you,
-Ron Newcomb
http://www.gamasutra.com/blogs/RonNewcomb/293/
http://www.ifwiki.org/index.php/Inform_7_for_Programmers
--------------------------------------------------------------------------
mercury-users mailing list
Post messages to: mercury-users at csse.unimelb.edu.au
Administrative Queries: owner-mercury-users at csse.unimelb.edu.au
Subscriptions: mercury-users-request at csse.unimelb.edu.au
--------------------------------------------------------------------------
More information about the users
mailing list