From fare@tunes.org Mon, 31 Jan 2000 17:25:57 +0100 Date: Mon, 31 Jan 2000 17:25:57 +0100 From: Francois-Rene Rideau fare@tunes.org Subject: Patterns and Compositionality > I didn't say it was impossible -- I said that it's not easy. I didn't say it was easy with lambda-like formal calculi. I said it was as easy/difficult with such calculi as with any formal calculi, including sequent calculi that you like. > proofs involving the resulting messes are a nightmare. Not any _more_ than with any calculus for imperative programming. Imperative programming IS a nightmare, stop. Using an imperative calculus as a "primitive" calculus is an abstraction inversion. > Nonsense! Computers and even turing machines handle those _easily_. Not easily. The semantics of computers and turing machines is quite contorted. >> Friends have done proofs of imperative programs (heap >> sorting, and more) >> in Coq (a logic system based on pure typed lambda-calculus); >> it's possible, >> but it's certainly not straightforward. > > Have you seen the proofs on similar systems written using abstract state > machines? The proofs are readable by anyone with a reasonable amount of > perseverence. > So are proof sketches in frameworks such as the one used by Coq. The part that's not straightforward is going from the proof sketch to the precise, machine-checkable proof. Having worked with proof checkers for an imperative framework (B, a dialect of Z), I can tell that the latter is certainly not simpler. > Fare', you're too hung up on formality. Not everything has a formal > definition! Not everything. But everything that can be usefully used for technical collaboration over e-mail. > "Design Patterns" are like "sewing patterns". Opposing them is > like opposing sewing patterns because they're only shapes, and geometricians > have known about shapes all along. In as much as there is a meaning in "Design Patterns", I do not oppose them (see below). What I oppose is the very approach of hyping around "Design Patterns", and presenting it as something that's new and the be all end all of programming methodology, whereas it's plain old and has been the very basis of programming since the very beginning (long known as "pseudo-code"). Similarly, just because I oppose "OO", "programming by contract", " Methodology", "Java", and the rest of the industrial hype doesn't mean that I reject the good things about them (that none of them brought about, but instead borrowed from better previous works). > A realistic definition of a "pattern" in that sense is "how it's been done > before". Perhaps the words "role model" serve better. No. A realistic definition of a "pattern" is meta-information (that you can alternatively consider as snippets of metaprograms for an arbitrarily declarative language). Yes, meta-information is useful. But just giving it a brand new name and hyping around informally instead of building formal tools to take advantage of it, is not progress but a brake to progress. >>> Hype is the entire basis of our devotion to Tunes, >>> and Fare is responsible for that. >>> So hype can be used for good as well as evil ;-). > >> No comment. > It's one of the more important things I said. > > Patterns are useful and expedient. TUNES is also useful, but not always > expedient. Hype claims to be useful, but is never expedient. TUNES is > closer to hype than patterns are. > Meta-comment: I am convinced that Tunes is not hype, but I admit I haven't given a proof of it yet; thus I reserve a comment for later (when I am no more convinced, of have given a suitable proof). And yes, I'm responsible for all of Tunes' shortcomings. Regards, [ François-René ÐVB Rideau | Reflection&Cybernethics | http://fare.tunes.org ] [ TUNES project for a Free Reflective Computing System | http://tunes.org ] How do you know that what you call ``blue'' is the same as what someone else calls ``blue''? What if someone else's feelings for red and blue are interverted as compared to yours? These questions are irrelevant, because the meaning of things doesn't lie in an unobservable intrinsic nature, but precisely in their observable external behavior. There is no achievable objectivity beyond the union of all possible intersubjectivities. -- Faré From btanksley@hifn.com Mon, 31 Jan 2000 13:46:53 -0800 Date: Mon, 31 Jan 2000 13:46:53 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Patterns and Compositionality > From: Francois-Rene Rideau [mailto:fare@tunes.org] > > I didn't say it was impossible -- I said that it's not easy. > I didn't say it was easy with lambda-like formal calculi. > I said it was as easy/difficult with such calculi as with any > formal calculi, including sequent calculi that you like. No, it's _not_ equally easy. Other formalisms are easier or harder. Be realistic: all things are not equal. Some proof systems are better at some things than other proof systems. Lambda calculus is really nice for name-based systems; other formalisms are better for other systems. It so happens that computers don't use names in any of their operations, so lambda calculus is a very poor fit. The only reason it's so popular, IMO, is that most people mistake the programming language for the computer. > > proofs involving the resulting messes are a nightmare. > Not any _more_ than with any calculus for imperative programming. > Imperative programming IS a nightmare, stop. I wasn't only talking about imperative coding, but the problem _is_ typified by it. Computers operate imperitively, stop. If that gives you nightmares, change careers. > Using an imperative calculus as a "primitive" calculus > is an abstraction inversion. In that case we need more abstraction inversions. That is to say, as nice as abstraction is, abstractions proportionally closer to useless as their distance from the thing they're modelling increases. > > Nonsense! Computers and even turing machines handle those _easily_. > Not easily. The semantics of computers and turing machines is > quite contorted. When looked at from the POV of lambda calculus, yes. This is why lambda calculus is so inadequate. > >> Friends have done proofs of imperative programs (heap > >> sorting, and more) > >> in Coq (a logic system based on pure typed lambda-calculus); > >> it's possible, > >> but it's certainly not straightforward. > > Have you seen the proofs on similar systems written using > > abstract state > > machines? The proofs are readable by anyone with a > > reasonable amount of perseverence. > So are proof sketches in frameworks such as the one used by Coq. I was afraid you'd say something like that, because different people have different definitions of what's 'reasonable'. My point was that proofs using ASMs are a _lot_ easier to read than typical proofs using only lambda calculus or combinators. (Keep in mind that ASMs usually simply reduce the problem to one which is easy to solve using other tools, such as combinators.) > The part that's not straightforward is going from the proof sketch > to the precise, machine-checkable proof. Having worked with > proof checkers > for an imperative framework (B, a dialect of Z), I can tell > that the latter is certainly not simpler. Using many modern calculi, certainly not. Using ASMs it's a LOT easier; take a look at any of the _many_ proofs on the ASM pages. > > Fare', you're too hung up on formality. Not everything has a formal > > definition! > Not everything. But everything that can be usefully used > for technical collaboration over e-mail. Do you know what the Visitor design pattern is? If so, then you can discuss a design which uses it easily. We can do that because we have a general pattern in common, given by a book. (We could also mention that the Visitor pattern is not needed in languages which support multiple dispatch -- but they in turn make other design patterns possible which weren't possible in C++ or Smalltalk.) > > "Design Patterns" are like "sewing patterns". Opposing them is > > like opposing sewing patterns because they're only shapes, > > and geometricians have known about shapes all along. > In as much as there is a meaning in "Design Patterns", I do > not oppose them > (see below). What I oppose is the very approach of hyping around > "Design Patterns", and presenting it as something that's new > and the be all end all of programming methodology, Then we are agreed. I also hate that. At the same time, I highly approve of attempts to document, describe, and name existing patterns. That ongoing effort makes design with the help of a group possible, and gives some assurance that the design has at least some quality to it. > whereas it's plain old and has been > the very basis of programming since the very beginning (long known as > "pseudo-code"). Um... No. Not even close. The old system this augments is simply nomenclature. (And it doesn't replace or imitate, it only augments.) > Similarly, just because I oppose "OO", > "programming by contract", " Methodology", "Java", > and the rest of the industrial hype doesn't mean that I reject > the good things about them (that none of them brought about, > but instead borrowed from better previous works). I don't oppose any of them. I accept them and use them. Just because they imitate something older doesn't automatically mean they do something else worse. > > A realistic definition of a "pattern" in that sense is "how > > it's been done > > before". Perhaps the words "role model" serve better. > No. You offer your own definition below, but before I attack it ;-) I'd like to hear why you think my definition stinks so bad you don't even bother to rebut it. > A realistic definition of a "pattern" is meta-information > (that you can alternatively consider as snippets of metaprograms > for an arbitrarily declarative language). > Yes, meta-information is useful. But just giving it a brand new name > and hyping around informally instead of building formal tools to take > advantage of it, is not progress but a brake to progress. Ah, Progress! The magic word of the 18th century. Well, I've been rebuked; I'm an impediment to progress. In that case, I'm obviously an impediment to Good, because Progress is Good because Good is Progress. (Okay, I got that out of my system.) Design Patterns are a _specific_ type of metainformation about a specific type of information. You're objecting to a perfectly good specific word simply because a vague, general word exists? If you object to the name "Design Patterns" on that basis, you should also object to "Binomial nomenclature" in zoology, because it's also metainformation. > >>> Hype is the entire basis of our devotion to Tunes, > >>> and Fare is responsible for that. > >>> So hype can be used for good as well as evil ;-). > >> No comment. > > It's one of the more important things I said. > > Patterns are useful and expedient. TUNES is also useful, > > but not always expedient. Hype claims to be useful, but > > is never expedient. TUNES is > > closer to hype than patterns are. > Meta-comment: I am convinced that Tunes is not hype, > but I admit I haven't given a proof of it yet; > thus I reserve a comment for later (when I am no more convinced, > of have given a suitable proof). By the definition of Hype and the definition of Tunes, Tunes is hype: it's a lot of talk about something which doesn't exist. > And yes, I'm responsible for all > of Tunes' shortcomings. Perhaps. I'd like to point out that you're responsible for all of Tunes' good things, too. And those cannot be minimized -- if it weren't for the Tunes project, many good things would not exist. These things were not brought about by formality; they were brought about by a collection and organization of information in a way which allowed humans to understand them. _That_ is the purpose of Design Patterns, and _that_ is also the area in which Tunes has been useful. But I just said that Tunes is hype. How do I reconcile that with my belief that the Tunes project is already useful? It's simpler than it looks: in reality, the best things a group can do start with hype, as the leader of the group encourages the members to look ahead to the future, to what amazing things are possible (but not yet real). You're very good at it, and without that no group can survive. > [ François-René ÐVB Rideau | Reflection&Cybernethics | -Billy From btanksley@hifn.com Mon, 31 Jan 2000 14:05:34 -0800 Date: Mon, 31 Jan 2000 14:05:34 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Joy, Lambdas, Combinators, Procedures > From: iepos@tunes.org [mailto:iepos@tunes.org] > > > Eh? Hmm, Haskell, for example, is not purely applicative. It has > > > a lambda construct, and also a strange (pattern-matching) > > > definition > > > construct. This is along with its strange constructs that involve > > > types. These constructs, it seems, are not syntactic sugar, but > > > essential constructs to the system. > > Oh, and BTW, the very existence of a lambda construct goes > > a long way to > > proving that Haskell is applicative. A lambda expression > > is only meaningful when it can be applied. > I believe this reply is based on a misunderstanding of what I meant > by "purely applicative". By a "purely applicative" language I mean > a language which has _only_ an application construct, and no other > constructs (other than primitives). So, since Haskell has a > lambda construct (a construct other than application), it is > not purely applicative. Purely applicative languages (in the sense > that I mean) do not have lambdas or named variables of any kind. Oh! So indeed we _were_ talking past each other. Thanks for the clarification -- I was talking about "applicative" languages versus "concatenative" languages, and you were talking about "purely applicative" languages versus "everything else". Now that we both know what each is trying to communicate, perhaps we can resolve these issues. Let me make clear my bias against a purely applicative language with a single disgusting analogy: I would no more consider using a purely applicative language for the sake of simplicity than I would consider using castration for the sake of moral purity. I consider both goals to be noble (in their place), but I consider the means to be misguided. All those features are in there for a reason: the reason is generally to make real programs simpler. > > > In case this was not clear, by "purely applicative > > > language", I mean > > > a language in which all expressions are built from a finite set > > > of primitives using a binary construct ("function application"). > > > An expression in such a language is most naturally written > > > using a binary tree, with primitives as leafs. > > This is a definition which fails to define. Joy fits that > > definition > > exactly, as does every language which uses finite-length > > identifiers. > No, Joy has an odd construct for quotation, with those "[]"s. > This is a construct other than application. You didn't say that it couldn't have other constructs. But never mind. Your purely applicative language has MANY other constructs, ranging from combinators to parenthesis to definitions. So your language doesn't fit your reading of that definition either. > > Unless you're implying that infix isn't allowed, in which case most > > languages are excluded (but not Joy). > Strictly, infix constructs other than application are not allowed > in a purely applicative language; however, a language that has > an infix construct can usually be reformalized without it, using > a new function primitive. Agreed. > > An applicative language any language which has application. > > Application is > I'll take that as a definition here... But remember that that is not > what I mean by a "purely applicative" language. It's an inclusive definition -- your purely applicative language will fit it. > > the action of assigning a language element as being > > exclusively the property > > of a function invocation. For example, in the C function call > > "unlink(filename+1);" the function 'unlink' is being applied to the > > syntactic element 'filename+1'. No other function is being > > given that > > particular element; in fact, it may be impossible to > > reproduce, as for > > example the C call "unlink(filename+rand());". > I'm not sure what you mean by this... This seems to be part of > an extended definition of "application", I guess... Exactly. It's a vital thing to realize about applicative languages: "application" is when you syntactically "apply" a paramater to a function. > > > > > [purely applicative systems] > > > > The benefits are very > > > > well-known -- but the drawbacks are as well. > > > Hmm, what drawbacks are you speaking of? > > First of all, the names. Because parameters are > > syntactically tied to the > > function to which they're applied, if you're going to use > > them more than > > once you need to give them a name. The presence of this > > name forces you to > > apply certain rules to the language's semantics beyond > > those which are > > strictly required by the mathematics. > Once again, a purely applicative system (in the sense that I meant) > does not use names for its parameters. Purely applicative > systems instead tend to use combinators (primitive functions such > as "B", which composes a function, and "C", which swaps its parameter > order, and others) to form functions. With a handful of > such primitive functions, it is possible to form any function > that could have been formed with a lambda (named variable) construct. This is true. If you'd played with J or APL you'd realize that it quickly becomes FAR to complicated to track parameters that way -- but it's still possible. > > In addition, because function invocation is complicated by function > > application, proofs written in the language have to deal > > with the double action. > The system you're describing (C, I believe) is nothing like a > purely applicative system. It's precisely and exactly (in this quote at least) purely applicative. Note that I said nothing about names above -- the simple fact is that in an applicative language, function invocation is always accompanied by function application; in other words, the language can't emit code to run the function until it has the paramaters which will be passed to the function. (Named variables actually don't eliminate applicative purity; they eliminate combinatoral purity, though, and I'm quite willing to agree that your language design can eliminate named variables.) > > > Anyway, one clear benefit of using a purely applicative language > > > is that it has such an amazingly simple syntax. > > I strongly suspect that you don't have a referent for > > "simple syntax". > > Currying (the simplest known way to define an applicative > > language) is > > indeed simpler than some of the complex parsing tricks some > > languages have > > to do; however, currying is not enough by itself. You also > > have to have > > four other syntactic elements: one to delimit functions, > > one to define > > names, one to count how many parameters a function has (and > > identify each > > one), and one to change the curry order. That's a total of > > five elements. > Again, this is based on a misunderstanding... A purely applicative > system does not use named parameters, and does have a truely > simple syntax. Read what I wrote -- I didn't mention named parameters. I listed five essential elements of application; with all five of those you don't have a language, instead you have a spreadsheet macro (or something). A concatenative language only needs some of those; it doesn't need curry order changes, and it doesn't need parameter counts. In place of currying, of course, it uses composition. Your language, by the way, adds another element which isn't listed in my "must have"s: combinators. Combinators seem to be special to your language, like IMMEDIATE words are to Forth. That definitely adds another element to your language -- an element which Joy does not have. (Joy includes functions which act as combinators, but they do so without any special language support.) > I think maybe another example may help... Thank you. > In a Joy test library, I saw this definition of "factorial": > [ [pop 0 =] [pop succ] [[dup pred] dip i *] ifte ] y Good choice. (That's an ugly way to do it and I wouldn't dream of writing real code that way, but it uses the primitives so it's easy to translate and is somewhat non-trivial.) Note that a harder challenge might have been appropriate for you: in Joy it's trivial to define new combinators; in fact, there's no distinction between a user-defined combinator and a user-defined function. Is it equally easy in your language? The definition of 'y' in Joy is: y == [dup cons] swap concat dup cons i; > In a purely applicative language, a factorial function might > be written as: > Y (S3 (ifte =0) (K 1) . S times . CB (minus 1)) > Or, without the sugar for composition ("."), > Y (B (S3 (ifte =0) (K 1)) (B (S times) (CB (minus 1)))) > where Y, B, S, S3, K, and C are certain primitive functions, > with these > rewrite rules: > Yf = f(Yf) > Bfgx = f(gx) > Sfgx = fx(gx) > S3 f g h x = fx(gx)(hx) > Cfxy = fyx > Hopefully you do not see a purely applicative approach as all > so bad now, seeing how similar in spirit it is to Joy. Well, I'm not sure that a single example can show that much. Can you refute my claim that Joy is simpler than even a truly pure applicative approach (and your language isn't pure, but rather adds the notion of combinators to an otherwise applicative base)? Furthermore, having looked at the proofs written in the Joy manuals, can you duplicate any of them as simply? > > > In fact, the > > > syntax of a purely applicative language is simpler than the syntax > > > of a Joy-style expression. The essential construct of Joy is > > > the list ("quoted program"); the other constructs (such as the > > > finite-set construct and number-literal construct) could be > > > eliminated. > > > So, the format of a Joy expression is a tree (with primitives > > > at the leafs), > > Something happened to the rest of this paragraph. I'm not > > sure what you > > were trying to say, so I'd better not try to reply :-). > Let me clarify... > It should be clear that the list construct (written using "[" and "]") > is the most fundamental construct of Joy. > If the extras of Joy were > omitted, the syntax of a Joy program would be basically a list > of expressions (programs to execute), where each individual > expression is either a primitive program (like "pop" or "dup") > or itself a list (using "[]"s) of expressions. Okay, I think I see your point (although I wouldn't call that foundational; it's only an essential side-effect of the concatenative nature of Joy). So you've now described Joy's syntax. > Anyhow, the whole point of this was to show that Joy's syntax is > a bit more complicated than a purely applicative one. Again, why? You've explained the syntax of Joy, but you haven't even made a reference to the syntax of your language, much less that of a purely applicative language. I've recited already the five syntactic elements that every applicative language needs in order to produce code; I've also recited the three elements that a concatenative language needs. I've also made the claim that your language needs one additional element (combinators) which is distinct from any of the other five needed by a truly pure applicative language. Can you make any counterclaims? > > Yes. It's an obsolete variant of a now-commercial system, > > but it still > > works. It's at ftp://archive.uwaterloo.ca/languages/j. > > (There may be other > > implementations there by now; however, it's a sophisticated > > language even though it has a simple syntax.) > I'll check it out... The interesting thing about APL is that it, like LISP, is an accidental pioneer; the inventor of APL didn't really intend for it to explore combinators any more than the inventor of LISP intended it to explore lambda-calculus. > - "iepos" (Brent Kerby) -Billy From btanksley@hifn.com Mon, 31 Jan 2000 15:57:22 -0800 Date: Mon, 31 Jan 2000 15:57:22 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Joy, Lambdas, Combinators, Procedures > From: Massimo Dentico [mailto:m.dentico@teseo.it] > btanksley@hifn.com wrote: > > [...] > > The interesting thing about APL is that it, like LISP, is > > an accidental > > pioneer; the inventor of APL didn't really intend for it to explore > > combinators any more than the inventor of LISP intended it > > to explore lambda-calculus. > Truly interesting for historical reasons. Can you give me > a reference for this? An MIT memo of late 50's/early 60's > I suppose. >From your mention of 'MIT' I assume you're talking about LISP rather than APL. There's some good historical info (well-footnoted) at http://faui80.informatik.uni-erlangen.de/html/lisp/histlit1.html which emphasises this point; McCarthy has also gone on record saying almost exactly that, although I've forgotten where. > Massimo Dentico -Billy From iepos@tunes.org Wed, 2 Feb 2000 15:56:05 -0800 (PST) Date: Wed, 2 Feb 2000 15:56:05 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Joy, Lambdas, Combinators, Procedures > Let me make clear my bias against a purely applicative language with a > single disgusting analogy: I would no more consider using a purely > applicative language for the sake of simplicity than I would consider using > castration for the sake of moral purity. I consider both goals to be noble > (in their place), but I consider the means to be misguided. > > All those features are in there for a reason: the reason is generally to > make real programs simpler. I agree that a "simplification" to a language's syntax is of little use if it is artificial, making the language more difficult to interpret. An example of this kind of simplification might be helpful: It has long been known that a system that uses many primitives can be reformalized as a system that uses a single primitive; for instance, consider a purely applicative system of arithmetic that has the primitives "1", "+", and "-". An example expression of the system might be: + 1 (- 1 1) which would denote "1". Now, the system could be reformalized using a single primitive, say "Z", such that ZZ = 1 Z(ZZ) = + Z(Z(ZZ) = - Essentialy, this "Z", given a primitive (or a thing that was a primitive in the original system), yields the next primitive. The expression "+ 1 (- 1 1)" in the reformalized system would be rewritten: Z(ZZ) (ZZ) (Z(Z(ZZ)) (ZZ) (ZZ)) This is a kind of "simplification" which I don't find to be too impressive; although it is possible to eliminate a lot of primitives, the new primitive "Z" is much more intuitively complex than any of the original primitives. A proof system based on the langauge using "Z" would probably not be any better than a similar proof system based on the original language, and would probably even be worse. Now, my view is that the simplification of a language to a purely applicative one is not like the simplification in this last example. I think that reducing a system to a purely applicative one may be truely useful (some reasons will be given at the end of the post). Still, it is not my view that non-purely-applicative systems are innately inferior... there may be other well-designed approaches that are also interesting (such as Joy's approach). My point is only that purely applicative languages are worth considering seriously; actually, the best way I know of to found a system is with a purely applicative language. > > > > In case this was not clear, by "purely applicative > > > > language", I mean > > > > a language in which all expressions are built from a finite set > > > > of primitives using a binary construct ("function application"). > > > > An expression in such a language is most naturally written > > > > using a binary tree, with primitives as leafs. > > Your purely applicative language has MANY other constructs, ranging from > combinators to parenthesis to definitions. So your language doesn't fit > your reading of that definition either. I suppose you are referring to the informal language I used in my posts; strictly, this language is not purely applicative, but is a sugared variant of a purely applicative system. The specific "other constructs" (combinators, parentheses, definitions) that you mentioned keep the language from being purely applicative are worth saying a few things about: First of all, expressions that denote combinators (such as "B", "C", "W", and "K") are primitives in the language. Recall in my definition of "purely applicative" that primitives (meaning atomic expressions that are assigned precise meanings) are allowed, as long as there are only a finite number of them. In fact, primitives are essential in a purely applicative system (Joy also relies heavily on primitives (like "dup", "swap", "dip", "map", etc.), rather than fancy constructs; this is in contrast to languages like C and even Haskell, which rely on myriads of fancy constructs). Next, parentheses... Parentheses, as they are used in my informal language, do indeed keep the system from being technically purely applicative. There are several ways to replace them; one way is to require that the application of "f" to "x" always be written as "(fx)", with omission of parentheses never allowed; this approach still uses parentheses, but in such a way that the binary construct of the language (representing function application) is a precise textual operation (specifically, concatenating "(" with the function expression, parameter expression, and ")"). There is another way to eliminate parentheses which is much better. Simply write the application of "f" to "x" as "@fx", prefixing "@" to the function expression and parameter expression. Of course, any atomic expression would work in place of "@" (we chose "@" because it suggests "application"). In a system with primitives "B", "C", "W", and "K", the expression that would be written with parentheses as "B(BW)(BC(BB))" would instead be written: @@B@BW@@BC@BB Noting that the rewritten version is not any larger than the original, this kind of notation may be of practical use not only in formal languages of systems, but in human-readable languages as well. Note that in this notation, "@@fxy" is the binary application (in a curried sense) of "f" to "x" and "y"; if this kind of notation is used in human readable systems, shorthands for "@@" and "@@@" would be nice. Third, definitions... Actually, I don't recall using any sort of definition construct in my language, but it is something that will be needed eventually... In any case, I don't think a special construct is needed for definitions. In some systems, it may be possible simply to use an equality primitive (a binary function yielding a proposition) instead. In my toy system, I plan on using a little different approach: a primitive "def" will be provided, which is a function that takes two parameters (a name to define and a thing to assign it to denote), yielding a procedure instructing the system to modify the system with the definition (and by "procedure", I still mean a function onto a procedure completion, so that procedures can be sequenced with composition). Also, another primitive "undef" will be used to undefine terms... There may be other primitives for inspecting the system's namespace; these specific procedures actually will probably not be taken as primitives... probably rather I'll have a "dict" primitive (a field storing the current dictionary) which can be inspected and modified with ordinary field primitives. Anyway, definitions, in a way, are not really that a fundamental part of a system (at least, not the system I'm working on), although they are quite a visible part. Actually, a finished system would of course not have just one static system dictionary; each user of the system at least would have their own. The whole issue of how the dictionaries (and thus definitions) will be handled will be taken care of by the system's shell(s), which interact with the users, and should (in the end) be quite flexible. Anyway, that takes care of those three obstacles... > > Once again, a purely applicative system (in the sense that I meant) > > does not use names for its parameters. Purely applicative > > systems instead tend to use combinators (primitive functions such > > as "B", which composes a function, and "C", which swaps its parameter > > order, and others) to form functions. With a handful of > > such primitive functions, it is possible to form any function > > that could have been formed with a lambda (named variable) construct. > > This is true. If you'd played with J or APL you'd realize that it quickly > becomes FAR to complicated to track parameters that way -- but it's still > possible. I think this may be a good point. I admit that I've done very little practical work with systems that use combinators to form functions (rather than lambdas). But I'm hopeful... > > > In addition, because function invocation is complicated by function > > > application, proofs written in the language have to deal > > > with the double action. > > > The system you're describing (C, I believe) is nothing like a > > purely applicative system. > > It's precisely and exactly (in this quote at least) purely applicative. > Note that I said nothing about names above -- the simple fact is that in an > applicative language, function invocation is always accompanied by function > application; in other words, the language can't emit code to run the > function until it has the paramaters which will be passed to the function. I'm really not sure what you mean by "function invocation" as distinguished from "function application", and I don't know what you mean by "run the function"... By "function", I usually mean an abstract mapping from things to other things. The things that will be "ran" in the system will be procedures, not generally functions. > (Named variables actually don't eliminate applicative purity; they eliminate > combinatoral purity, though, and I'm quite willing to agree that your > language design can eliminate named variables.) Hmm... named variables do destroy applicative purity, at least if they are accompanied by a lambda construct to bind them. Anyhow, I think you agree that whether they destroy applicative purity or not they greatly clutter programs and make proofs about the system much more difficult. > > > > Anyway, one clear benefit of using a purely applicative language > > > > is that it has such an amazingly simple syntax. > > > > I strongly suspect that you don't have a referent for > > > "simple syntax". > > > Currying (the simplest known way to define an applicative > > > language) is > > > indeed simpler than some of the complex parsing tricks some > > > languages have > > > to do; however, currying is not enough by itself. You also > > > have to have > > > four other syntactic elements: one to delimit functions, > > > one to define > > > names, one to count how many parameters a function has (and > > > identify each > > > one), and one to change the curry order. That's a total of > > > five elements. > > > Again, this is based on a misunderstanding... A purely applicative > > system does not use named parameters, and does have a truely > > simple syntax. > > Read what I wrote -- I didn't mention named parameters. I listed five > essential elements of application; with all five of those you don't have a > language, instead you have a spreadsheet macro (or something). I'm not really sure what you mean by each of those syntactic elements and certainly don't see that they are required in all purely applicative systems. I've given the syntax of a small purely applicative language above, with these being some expressions in it: @BW @@BCC @B@WB K @@B@BW@@BC@BB How are your 5 syntactic elements manifest in this language? > Your language, by the way, adds another element which isn't listed in my > "must have"s: combinators. Combinators seem to be special to your language, > like IMMEDIATE words are to Forth. That definitely adds another element to > your language -- an element which Joy does not have. (Joy includes > functions which act as combinators, but they do so without any special > language support.) Combinators in my purely applicative language are treated as ordinary primitives; they do not have special language support any more than Joy has special support for "dip", "swap", "dup", and "pop". > > In a Joy test library, I saw this definition of "factorial": > > > [ [pop 0 =] [pop succ] [[dup pred] dip i *] ifte ] y > > Good choice. (That's an ugly way to do it and I wouldn't dream of writing > real code that way, but it uses the primitives so it's easy to translate and > is somewhat non-trivial.) Note that a harder challenge might have been > appropriate for you: in Joy it's trivial to define new combinators; in fact, > there's no distinction between a user-defined combinator and a user-defined > function. Is it equally easy in your language? Yes, it will be just as easy, provided some syntactic sugar is permitted in the language. Interestingly, Joy has a special construct for definitions, which could have been avoided... For example, one might define "popd" as "[dip] pop" in Joy by: popd == [dip] pop Yet this could have achieved without treating "==" specially, if they had added a "define" primitive, in this sort of way: "popd" [[dip] pop] define "define" could be treated as an atomic program (that has an effect other than on the stack, like "put" and "get") that modifies the interpreter's namespace... Of course, one would still like to have "==" as sugar. This is the approach I plan on using for definitions in my system. As a side note, you may wonder what the use there would be in eliminating "==" as a special construct... If the system provided a construct like "define" in place of "==", then it would be possible for the system to talk about its own definitions; for instance, ["popd" [[dip] pop] define] would be a first-class expression in the system, a quotation of a program that makes defines "popd" to mean "[dip] pop". One of the main benefits the author boasts of the system is the ability of the system to talk about its own programs directly, using quotation, without the need of encoding like "Goedel numbering". Adding "define" as a first-class primitive would extend this ability in a natural way. > The definition of 'y' in Joy is: > > y == [dup cons] swap concat dup cons i; Similarly, in my system, the Y combinator might be defined as: Y == BM(CBM) where "M" is the combinator that applies a function to itself: M == WI The reason that the definition in my system appears shorter is, of course, only because of the convention of naming combinators with a single letter (whereas Joy used longer identifiers); this convention, when identifiers are ran together without spaces, may lead to some problems when the system also uses multiple-letter identifiers (it may become impossible, given a expression, to determine where one identifier ends and the next begins). This problem can be solved with the convention that all identifiers begin with a capital letter but contain no more. But, this convention would make parsing a bit complicated; in a pure system that uses "@", a better approach would be terminate each primitive expression with some terminator character that is not used anywhere else, or to otherwise encode the primitive expressions so that they are self-delimiting. The two definitions above of "Y" and "M" are a bit sugared; in unsugared form they might appear: @@Define "Y" @@BM@@CBM @@Define "M" @WI This would still not be quite fully unsugared, because of the string literals "Y" and "M"... Like Joy, my system will probably use lists of characters for strings; so, string literals will be sugar for a list of character primitives... Lists will probably be taken as functions that pass their elements as parameters. So, using the T combinator (which forms a unit list), the two definitions above could be written in fully unsugared form as: @@Define@TYlet@@BM@@CBM @@Define@TMlet@WI These two expressions are each purely applicative. The first could be written using a binary tree as (warning: ASCII art) @ / \ / \ / \ / \ / \ / \ / \ / \ / \ @ @ / \ / \ / \ / \ / \ / \ define @ @ @ / \ / \ / \ T Ylet B M @ M / \ C B String literals would be a bit messier to unsugar if they were more than one letter long (one would have to use a variation of the "T" combinator, applying it to each of the character primitives in turn, using currying). Anyway, I'm still a bit undecided on how exactly to deal with lists in the system; I may end up prefixing the size of the list as the first element, or else may treat a list the classical way, as a chain of pairs (made from "cons"), ending in a "nil". > > In a purely applicative language, a factorial function might > > be written as: > > Y (S3 (ifte =0) (K 1) . S times . CB (minus 1)) Oops... this is not quite right; should be: Y (S3 (ifte.=0) (K 1) . S times . CB (minus 1)) ^ Also, "S3" will probably not really work because it would be construed as the application of "S" to "3", but this is quite a syntactic matter... Of course, the factorial function could be written more concisely using a function like Joy's "primrec" as: Primrec (= 0) (K 1) Times The parentheses could probably be omitted, the spacing giving the structure: Primrec Q0 K1 Times where "Q" is the equality primitive (used out of reluctance to use "=" in a syntax other than infix). Anyway, once again, this would be sugar for: @@@Primrec@Q0@K1Times Or, written using a binary tree: @ / \ / \ @ Times / \ / \ / \ @ @ / \ / \ / \ K 1 Primrec @ / \ Q 0 > > Hopefully you do not see a purely applicative approach as all > > so bad now, seeing how similar in spirit it is to Joy. > > Well, I'm not sure that a single example can show that much. Can you refute > my claim that Joy is simpler than even a truly pure applicative approach > (and your language isn't pure, but rather adds the notion of combinators to > an otherwise applicative base)? Well, that depends on what precisely you mean by "simpler". A purely applicative approach is simpler in the sense that the only constructs needed are primitives and function-application, while the Joy approach needs at least primitives, concatenation, and quotation... You seem hesitant to accept that quotation is a real construct of Joy... Perhaps a quote from the manual may help (beginning of "Mathematical foundations of Joy" in the synopsis): Joy programs are built from smaller programs by just two constructors: concatenation and quotation. Concatenation is a binary constructor, and since it is associative it is best written in infix notation and hence no parentheses are required. Since concatenation is the only binary constructor of its kind, in Joy it is best written without an explicit symbol. Quotation is a unary constructor which takes as its operand a program. In Joy the quotation of a program is written by enclosing it in square brackets. Ultimately all programs are built from atomic programs which do not have any parts. In Joy, as the authors have formalized it, expressions are built from primitives using two constructs: concatenation and quotation. In purely applicative systems, only application is needed. Parentheses often play the role of grouping that quotation does in Joy, but parentheses can be eliminated as I've shown above. > Furthermore, having looked at the proofs written in the Joy manuals, can you > duplicate any of them as simply? I believe so... I'm not sure exactly which proofs you're talking about... One proof that stands out was the "naturality of 'cons'". Here was the proof they gave in the manual (in "The Algebra of Joy"): Somewhat more difficult is the naturality of cons. In conventional notation this is expressed by map(f,cons(x,L)) = cons(f(x),map(f,L)) and in Joy notation by [cons] dip map == dup [dip] dip map cons This is so complex that a step-by-step verification is called for. Let L and x be the list and the additional member. Let [F] be a program which computes the function f. Let x' be the result of applying f to x, and let L' be the result of applying f to all members of L. The proof of the equivalence of the LHS and the RHS consists in showing that both reduce to the same program. For the LHS we have: x L [F] [cons] dip map LHS == x L cons [F] map (dip) == [x L] [F] map (cons) == [x' L'] (map) For the RHS: x L [F] dup [dip] dip map cons RHS == x L [F] [F] [dip] dip map cons (dup) == x L [F] dip [F] map cons (dip) == x' L [F] map cons (dip) == x' L' cons (map) == [x' L'] (cons) The two sides reduce to the same program, so they denote the same function. Note that they resorted to funny notations for x' and L'. Also the rule referred to as "(map)" doesn't seem to be a precise syntactic inference rule (it is based on their strange notation of "x'" and "L'"). Actually, the rule seems to be a vague version of the precise thing that they are trying to prove; so, they're assuming a slightly weakened version of the thing they are trying to prove, making the whole proof quite silly. I'm not sure if it would be possible to restate the Joy proof using more precise methods. Anyway, here is a proof of an analagous result in a purely applicative system: The fact to be shown is that MapF ConsXY is replacable in the system by Cons FX MapFY for all functions "F", things "X", and lists "Y". A stronger version of this result, that it holds for all "F", "X", and "Y" (which may or may not be provable in a purely applicative system, depending on its degree of "typedness"; for simplicity, I'll assume here that we're dealing with a quite untyped system) can be written using combinators by saying that CBCons.B.Map is replacable by S (Bo2 Cons) Map Now, this property is almost the defining property of "Map", but we won't just say that it holds "by definition", because "Map" will probably not really be taken as a primitive of the system. So, we'll give a recursive definition of "Map" in terms of "Cons", "Nil", "Fst", and "Snd" and then show how the desired property follows from the properties of these primitives. Now, "Cons", "Nil", "Fst", and "Snd" will probably not be primitives of the system either, but for now we'll take their characteristic properties as axiom schemes (since the Joy proof did no better): (Fst): Fst (Cons X Y) = X (Snd): Snd (Cons X Y) = Y (Nil): (Cons X Y = Nil) = CK This last property is a way of saying that no pair formed from "Cons" can be equal to "Nil". This system uses the "K" and "CK" combinators for the truth and false proposition values, respectively. (This is a convenient representation because then "P X Y" results in "X", if "P" is the truth proposition, and "Y" if "P" is the false proposition; so, simply applying a proposition to two parameters can be used for as conditional, without a need of a special "ifte" primitive). Also, there will be axiom schemes for some combinators: (B) : BFGX = F(GX) (C) : CFXY = FYX (S) : SFGX = FX(GX) (Bo2): Bo2FGHXY = F(GX)(HY) (N) : NFGHX = F(GX)(HX) (Linrec): LinrecFGXYZ = FZ GZ (XZ (LinrecFGXY YZ)) These combinators will probably not all be taken as primitive, so these schemes may be derived rather than postulated as axioms. Now, we can define "Map" using "Linrec" as: C (Linrec QNil KNil) Snd . BCons . CBFst Now, finally, we prove the result, that "MapF ConsXY" is replacable by: (C (Linrec QNil KNil) Snd . BCons . CBFst)F ConsXY (definition of Map) C (Linrec QNil KNil) Snd (BCons (CBFst F)) ConsXY (B) Linrec QNil KNil (BCons (CBFst F)) Snd ConsXY (C) QNil ConsXY (KNil ConsXY) (BCons (CBFst F) ConsXY (...)) (Linrec) CK (KNil ConsXY) (BCons (CBFst F) ConsXY (...)) (Nil) K (BCons (CBFst F) ConsXY (...)) (KNil ConsXY) (C) BCons (CBFst F) ConsXY (...) (K) Cons (CBFst F ConsXY) (...) (B) Cons (B F Fst ConsXY) (...) (C) Cons (F (Fst ConsXY)) (...) (B) Cons FX (...) (Fst) Cons FX (Linrec QNil KNil (BCons (CBFst F)) Snd (Snd ConsXY)) (see note) Cons FX (C (Linrec QNil KNil) Snd (BCons (CBFstF)) (Snd ConsXY)) (C) Cons FX ((C (Linrec QNil KNil) Snd . BCons . CBFst)F (Snd ConsXY)) (B) Cons FX (MapF (Snd ConsXY)) (definition of Map) Cons FX MapFY (Snd) Part of the result of the fourth step was abbreviated as "..." until near the end... This proof was, of course, quite a bit messier than the Joy proof, but then again, it actually proves something, unlike the Joy proof... Perhaps this was not such a good choice of thing to proof, since it is not really analagous to the proof in the Joy manual; if you have a favorite proof in the Joy manual, maybe I'll try that one. Anyway, that was probably not exactly the cleanest way to do it, either. Anyway, both the proof I just gave and the Joy proof are informal proofs about systems... One interesting thing is that both proofs used variables (in the "epi-language", not in the formal language itself); in the case of the purely applicative system at least, the proof could be rewritten without variables at all using the Combinatory Axioms (and abstracted versions of the Fst, Snd, and Nil axiom schemas). This would have been a bit trickier, though, and I'm not really too familiar with the combinatory axioms, so I didn't attempt it. To give you a flavor of how this might work though, here are a couple combinatory axioms: B(BB)B = C(BB(BBB))B BCC = I The first one states the associativity of composition; for, from it it follows that B(BB)B F G H = C(BB(BBB))B F G H BB BF G H = BB(BBB)F B G H B(BFG)H = B(BBBF) B G H B(BFG)H = BBBF BG H B(BFG)H = B BF BG H B(BFG)H = BF(BGH) or in other words, that (F.G).H = F.(G.H) The other combinatory axiom mentioned (that "BCC = I") states that taking the converse of a function twice is the same as doing nothing at all (i.e., the composition of the "C" combinator with itself is the identity combinator). These two axioms are not particularly special, but with a handful of ones like them, a sort of completeness follows; it would be possible to directly show that CBCons.B.Map is replacable by S (Bo2 Cons) Map without the use of variables like "F", "X", and "Y". Anyway, hopefully this example has helped you gain a feel for the elegance a purely applicative system can have. Again, if you have a favorite proof from the Joy manual, feel free to tell me what it is... > I've recited already the five syntactic elements that every applicative > language needs in order to produce code; I've also recited the three > elements that a concatenative language needs. I've also made the claim that > your language needs one additional element (combinators) which is distinct > from any of the other five needed by a truly pure applicative language. > > Can you make any counterclaims? This was mentioned above... I don't see that those five "syntactic elements" are essential in every applicative language or how they are evident in the one I've presented... And, combinators are not particularly special elements in the language I've given, but ordinary primitives. Okay... Earlier I said that I'd give some reasons to like purely applicative systems at the end of the post, so here are some (these are reasons to prefer purely applicative systems over similar systems that are mainly applicative, but with some extensions, such as infix operators): 1) The syntax is simpler. In a computing system, this may be beneficial because it will make parsers, compilers, and other programs that deal with expressions (such as a program that transmits information between machines, over networks) easier to construct. (this is quite a minor one, compared to the next). 2) It ensures that the system can be made combinatorialy complete with a finite set of combinators. If a system has an infix operator that cannot be used as a first-class function, then it is not generally possible to emulate functional abstraction with combinators (rather, one would likely need to resort to a lambda construct). If the second reason does not make sense, consider a system that is mostly applicative, with combinators "B", "C", "W", and "K", but extended with an binary infix "+" operator. In this system it would not be possible to form the function that adds a number to itself by "+". One cannot throw combinators at it, because "+" cannot be isolated as a first-class function. One could, however, form such a doubling function in the system if it was further extended with a lambda construct. A doubling construct would be expressed by "x\ x+x" (where "x\ Z" means "the function that given 'x', yields 'Z'"). Yet, there are disadvantages, I think you'll agree, of basing a system on a lambda construct. The whole problem could be avoided if one formalized "+" as a first-class function that takes two parameters through currying, instead of as an infix operator. > > - "iepos" (Brent Kerby) > > -Billy So, what do you think of purely applicative systems now? - "iepos" (Brent Kerby) From btanksley@hifn.com Wed, 2 Feb 2000 16:14:07 -0800 Date: Wed, 2 Feb 2000 16:14:07 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Joy, Lambdas, Combinators, Procedures > So, what do you think of purely applicative systems now? I'm happier with them than I was before, enough so that I'm willing to concede that they might be more useful than impure applicative systems (as you said, we'll have to try defining one to be sure about that). I'm also thankful that you took the time again to explain. I'll reply with some specific concerns, questions, answers, and rebuttals later (gotta do my job now). ;-) > - "iepos" (Brent Kerby) -Billy From fare@tunes.org Fri, 4 Feb 2000 00:17:01 +0100 Date: Fri, 4 Feb 2000 00:17:01 +0100 From: Francois-Rene Rideau fare@tunes.org Subject: Joy, Lambdas, Combinators, Procedures > There is another way to eliminate parentheses which is much > better. Simply write the application of "f" to "x" as "@fx", > prefixing "@" to the function expression and parameter > expression. Of course, any atomic expression would work > in place of "@" (we chose "@" because it suggests "application"). The purely applicative programming language unlambda, based on combinators, uses ` instead of @ (actually, I fear that unlambda is not so pure, in that it has imperative I/O). See around http://www.eleves.ens.fr:8080/home/madore/programs/unlambda/ [ François-René ÐVB Rideau | Reflection&Cybernethics | http://fare.tunes.org ] [ TUNES project for a Free Reflective Computing System | http://tunes.org ] It is proof of a base and low mind for one to wish to think with the masses or majority, merely because the majority is the majority. Truth does not change because it is, or is not, believed by a majority of the people. -- Giordano Bruno (1548-burned at the stake,1600) From iepos@tunes.org Thu, 3 Feb 2000 20:45:05 -0800 (PST) Date: Thu, 3 Feb 2000 20:45:05 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Joy, Lambdas, Combinators, Procedures [Charset iso-8859-1 unsupported, filtering to ASCII...] > > There is another way to eliminate parentheses which is much > > better. Simply write the application of "f" to "x" as "@fx", > > prefixing "@" to the function expression and parameter > > expression. Of course, any atomic expression would work > > in place of "@" (we chose "@" because it suggests "application"). > The purely applicative programming language unlambda, based on combinators, > uses ` instead of @ (actually, I fear that unlambda is not so pure, in that > it has imperative I/O). See around > http://www.eleves.ens.fr:8080/home/madore/programs/unlambda/ thanks for mentioning it. They do use ` in the same way that i used @ what a silly system... The quining part is especially interesting. > [ Fran_ois-Ren_ _VB Rideau | Reflection&Cybernethics | http://fare.tunes.org ] - "iepos" (Brent Kerby) From btanksley@hifn.com Sun, 6 Feb 2000 22:14:24 -0800 Date: Sun, 6 Feb 2000 22:14:24 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Joy, Lambdas, Combinators, Procedures Subject: RE: Joy, Lambdas, Combinators, Procedures > From: iepos@tunes.org [mailto:iepos@tunes.org] > > Let me make clear my bias against a purely applicative > > language with a single disgusting analogy: > I agree that a "simplification" to a language's syntax is of little > use if it is artificial, making the language more difficult to > interpret. An example of this kind of simplification might be > helpful: Yes, it was. Thank you. > Now, my view is that the simplification of a language to a > purely applicative one is not like the simplification > in this last example. I think that reducing a system > to a purely applicative one may be truely useful (some > reasons will be given at the end of the post). Convincing, too. Thank you for taking the time to explain; I am much enlightened. > Still, it is not my view that non-purely-applicative systems > are innately inferior... there may be other well-designed > approaches that are also interesting (such as Joy's approach). > My point is only that purely applicative languages are > worth considering seriously; A worthy point; my point is that for 20 or 30 years we've been considering nothing but applicative languages. To me, the idea that another applicative language might solve the problems we've had with applicative languages in the past seems kind of odd, perhaps even naive. The interesting thing to me about your language to me isn't really its applicativeness; other languages are at least as applicative. The interesting thing is the strong presence of combinators. Perhaps your language would be better called a combinative language -- most people, when discussing languages, assume applicative and lambda-based. Your language is applicative and combinator-based; thus you say in one word 'combinator' the essence of your difference from most modern language designs. But I won't mention that again unless you ask about it; you're entitled to you choice of names, so long as they're reasonably accurate (and yours is). > actually, the best way I > know of to found a system is with a purely applicative language. It may be that purity will have a value which is not found in the impure versions, but which we've been searching for for the past 20 years. I find it hard to believe; we're not the smartest people to come down the pike since Socrates ;-). Nonetheless, I'm less inclined to argue against trying than I was. > > > > > In case this was not clear, by "purely applicative > > > > > language", I mean > > > > > a language in which all expressions are built from a > > > > > finite set > > > > > of primitives using a binary construct ("function > > > > > application"). > > > > > An expression in such a language is most naturally written > > > > > using a binary tree, with primitives as leafs. > > Your purely applicative language has MANY other constructs, > > ranging from > > combinators to parenthesis to definitions. So your > > language doesn't fit > > your reading of that definition either. > I suppose you are referring to the informal language I used > in my posts; strictly, this language is not purely applicative, > but is a sugared variant of a purely applicative system. > The specific "other constructs" (combinators, parentheses, > definitions) > that you mentioned keep the language from being purely applicative > are worth saying a few things about: > First of all, expressions that denote combinators (such as > "B", "C", "W", and "K") are primitives in the language. > Recall in my definition of "purely applicative" that > primitives (meaning atomic expressions that are assigned > precise meanings) are allowed, as long as there are only a > finite number of them. In fact, primitives are essential > in a purely applicative system (Joy also relies heavily > on primitives (like "dup", "swap", "dip", "map", etc.), > rather than fancy > constructs; this is in contrast to languages like C and even > Haskell, which rely on myriads of fancy constructs). WHOAH! Now I see what you meant. You really threw me off by using the word "finite" there. (I don't know of any languages which use an infinite number of primitives, by ANY definition of 'primitive'.) I suspect you'd dislike one of my potential changes to Joy: I wanted to remove all of the stack manipulation words and replace them with a single syntax convention: LET dup == stack(a--aa); drop == stack(a--); swap == stack(ab--ba); rot == stack(abc--bca); 4reverse == stack(abcd--dcba) IN your code END. The number of 'primitives' is still finite, but is now not bounded by the ones I choose (plus, I don't have to think of names). I also want to allow the user to define similar parsers, in a manner similar to how Forth parses numbers but user-definable. Anyhow, you have an interesting approach to 'primitives'; the way I see it, each different type of primitive is a seperate category; I don't group them all simply by calling them 'primitives'. Joy's primitives are all functions (or all combinators, depending on how you view it; they're really the same thing), and everything you define is also a function (and a combinator). Essentially, there's no distinction between primitive words and user words; the only distinction which exists in the system is the distinction between syntax and words. Joy has a lot of syntax; I personally suspect that I will keep the quotation syntax, and I'll add the concept of immediate execution and postponement, so that the coder can write words which act to modify the compilation process, and then I'll throw in user-definable lexing (as shown by the stack shufflers above). I'll talk a little more later about your definition of "primitives". > There is another way to eliminate parentheses which is much > better. Simply write the application of "f" to "x" as "@fx", > prefixing "@" to the function expression and parameter > expression. Of course, any atomic expression would work > in place of "@" (we chose "@" because it suggests "application"). Okay. This is kind of interesting, because it makes curring explicit and grouping implicit -- parentheses made grouping explicit and currying implicit. I can't say anything against the change, aside from commenting that if you hadn't done it I would have continued to miss some interesting things in your system. I don't think that's to to any special qualities of the notation, though, but rather the novelty. > In a system with primitives "B", "C", "W", and "K", > the expression that would be written with parentheses > as "B(BW)(BC(BB))" would instead be written: > @@B@BW@@BC@BB > Noting that the rewritten version is not any larger than the > original, this kind of notation may be of practical use > not only in formal languages of systems, but in human-readable > languages as well. Note that in this notation, "@@fxy" > is the binary application (in a curried sense) of "f" to "x" and "y"; > if this kind of notation is used in human readable systems, > shorthands for "@@" and "@@@" would be nice. > Third, definitions... Actually, I don't recall using any sort > of definition construct in my language, but it is something > that will be needed eventually... In any case, I don't > think a special construct is needed for definitions. It actually is special -- you're disregarding the fact that any or all of your 'primitives' could actually be 'special constructs'. You imply that by calling a language feature a "primitive" and giving it a name, you evade any complications it causes. But I'm getting ahead of myself. In this case, the special action of the Define primitive is modification of the system dictionary, a dictionary which is specially distinguished because all word lookups start with it, even though it has no name of its own. > In some systems, it may be possible simply to use an equality > primitive (a binary function yielding a proposition) instead. I suspect that the author of Joy intended this with his use of ==. I don't think he succeeded, and worse still, he didn't provide any other way of defining things. (While I'm listing shortcomings, I also didn't like the lack of immediate execution -- like Forth's IMMEDIATE word.) Speaking of immediate execution, how would your system handle Forth-like metaprogramming? Does the code have full access to all of the unparsed source? > In my toy system, I plan on using a little different approach: > a primitive "def" will be provided, which is a function > that takes two parameters (a name to define and a thing to > assign it to denote), yielding a procedure instructing > the system to modify the system with the definition > (and by "procedure", I still mean a function onto a > procedure completion, so that procedures can be sequenced > with composition). Also, another primitive "undef" will > be used to undefine terms... There may be other primitives > for inspecting the system's namespace; these specific > procedures actually will probably not be taken as primitives... > probably rather I'll have a "dict" primitive (a field storing > the current dictionary) which can be inspected and modified > with ordinary field primitives. > Anyway, definitions, in a way, are not really that a fundamental > part of a system (at least, not the system I'm working on), > although they are quite a visible part. Actually, They actually are fundamental -- if they weren't the compiler wouldn't know how to look up the words you type. As I mentioned, the System Dictionary is a distinguished element in language theory. > a finished system would of course not have just one static > system dictionary; each user of the system at least would > have their own. The whole issue of how the dictionaries > (and thus definitions) will be handled will be taken care of by > the system's shell(s), which interact with the users, and should > (in the end) be quite flexible. Right. This is one problem with Joy. > > > Once again, a purely applicative system (in the sense > > > that I meant) > > > does not use names for its parameters. Purely applicative > > > systems instead tend to use combinators (primitive functions such > > > as "B", which composes a function, and "C", which swaps > > > its parameter > > > order, and others) to form functions. With a handful of > > > such primitive functions, it is possible to form any function > > > that could have been formed with a lambda (named > > > variable) construct. > > This is true. If you'd played with J or APL you'd realize > > that it quickly > > becomes FAR to complicated to track parameters that way -- > > but it's still > > possible. > I think this may be a good point. I admit that I've done very > little practical work with systems that use combinators to > form functions (rather than lambdas). But I'm hopeful... I have no reason do deny that hope; not even APL and J provide a full set of combinators, so they just might be deficient. Perhaps a full set will make it easy. Joy certainly makes it look easy, even though it doesn't depend on combinators for everything. > > > > In addition, because function invocation is complicated > > > > by function > > > > application, proofs written in the language have to deal > > > > with the double action. > > > The system you're describing (C, I believe) is nothing like a > > > purely applicative system. > > It's precisely and exactly (in this quote at least) purely > > applicative. > > Note that I said nothing about names above -- the simple > > fact is that in an > > applicative language, function invocation is always > > accompanied by function > > application; in other words, the language can't emit code to run the > > function until it has the paramaters which will be passed > > to the function. > I'm really not sure what you mean by "function invocation" as > distinguished from "function application", and I don't know > what you mean by "run the function"... > By "function", I usually mean an abstract mapping from things to other > things. The things that will be "ran" in the system will be > procedures, not generally functions. This is not something you mentioned before. So your language distinguishes between procedures and functions? How? Why? Doesn't that have negative implications as far as proofs go? Wouldn't it make metaprogramming harder? Is this distinction a necessary element of applicative languages of the type you describe? It's not present in any way in any currently existing concatenative language. > > (Named variables actually don't eliminate applicative > > purity; they eliminate > > combinatoral purity, though, and I'm quite willing to agree > > that your language design can eliminate named variables.) > Hmm... named variables do destroy applicative purity, at least if > they are accompanied by a lambda construct to bind them. Anyhow, > I think you agree that whether they destroy applicative purity or not > they greatly clutter programs and make proofs about the system > much more difficult. I agree with that. > > > > > Anyway, one clear benefit of using a purely > > > > > applicative language > > > > > is that it has such an amazingly simple syntax. > > > > I strongly suspect that you don't have a referent for > > > > "simple syntax". > > > > Currying (the simplest known way to define an applicative > > > > language) is > > > > indeed simpler than some of the complex parsing tricks some > > > > languages have > > > > to do; however, currying is not enough by itself. You also > > > > have to have > > > > four other syntactic elements: one to delimit functions, > > > > one to define > > > > names, one to count how many parameters a function has (and > > > > identify each > > > > one), and one to change the curry order. That's a total of > > > > five elements. > I'm not really sure what you mean by each of those syntactic elements > and certainly don't see that they are required in all purely > applicative systems. I've given the syntax of a small purely > applicative language above, with these being some expressions in it: > @BW > @@BCC > @B@WB > K > @@B@BW@@BC@BB > How are your 5 syntactic elements manifest in this language? 1) Currying is explicit. 2) Function delimitation is implied by a newline. 3) Name definition is not present (but you cover it later, and you don't deny that it's needed); note that in the examples you give, the functions are simply thrown away, no possibility of reuse. 4) Curry order modification is carried out by the absence of @ (as I mentioned, you've only made grouping implicit). 5) Parameter counts are needed to actually run/use any of your functions; your procedural sublanguage will need to understand them even if your applicative language doesn't recognise them. Also, your language will have to keep count just in case the programmer tries to apply too many arguments to a function. I'm seeing something I didn't notice up till now, though. I seem to see that function delimitation isn't needed in either of our languages -- grouping/quotation is sufficient (and grouping in your language carries out curry order modification). This leaves your language with 4 outstanding features and mine with 3; the missing element is parameter counts. In your language parameter counts are very interesting and complex; a parameter can be shunted through a _lot_ of combinators, some of them with a runtime action (such as the 'select' combinator); and the end result has to be known before the result can be run. I'm not sure whether this means that runtime combinators are impossible for you; and if it does mean that, I'm not sure whether that denies you completeness or makes the programmer's job harder. > > Your language, by the way, adds another element which isn't > > listed in my > > "must have"s: combinators. Combinators seem to be special > > to your language, > > like IMMEDIATE words are to Forth. That definitely adds > > another element to > > your language -- an element which Joy does not have. (Joy includes > > functions which act as combinators, but they do so without > > any special language support.) > Combinators in my purely applicative language are treated as ordinary > primitives; they do not have special language support any more > than Joy has special support for "dip", "swap", "dup", and "pop". Not true. Combinators in your language change the order of execution; they completely change the emitted code. You don't seem to see a distinction between primitives which drive the compiler and primitives which simply have runtime behavior. All of Joy's primitives have only runtime behavior; most of your combinators require compile-time behavior. This is the problem I was hinting about earlier, by the way. You define "primitive" such that it includes many very different words, including many words which are actually syntactic in their effect. So although Joy's primitives all include compiled support, none of them require compile-time behavior. They all do only one job: juggle the stack at runtime. If someone invents a new combinator which nobody in the past ever imagined, it'll be a simple thing to add it to Joy (witness my little stack-shuffler, which essentially allows the trivial definition of new combinators), but to add it to a syntax-based language will be a headache. > > > In a Joy test library, I saw this definition of "factorial": > > > [ [pop 0 =] [pop succ] [[dup pred] dip i *] ifte ] y > > Good choice. (That's an ugly way to do it and I wouldn't > > dream of writing > > real code that way, but it uses the primitives so it's easy > > to translate and > > is somewhat non-trivial.) Note that a harder challenge > > might have been > > appropriate for you: in Joy it's trivial to define new > > combinators; in fact, > > there's no distinction between a user-defined combinator > > and a user-defined > > function. Is it equally easy in your language? > Yes, it will be just as easy, provided some syntactic sugar is > permitted in the language. Interestingly, Joy has a special > construct for definitions, which could have been avoided... > For example, one might define "popd" as "[dip] pop" in Joy by: > popd == [dip] pop > Yet this could have achieved without treating "==" specially, > if they had added a "define" primitive, in this sort of way: > "popd" [[dip] pop] define > "define" could be treated as an atomic program (that has an effect > other than on the stack, like "put" and "get") that modifies > the interpreter's namespace... Of course, one would still like > to have "==" as sugar. This is the approach I plan on using for > definitions in my system. But in Joy "==" is more than sugar -- it's a fundamental part of the notation. The model of Joy is that functions take a single parameter (the stack) and return a single parameter (another stack); there's not supposed to be a special dictionary available to them; the dictionary is supposed to be immutable. The '==' syntax is intended to make Joy programs look like proofs; notice how the language syntax is set up so that it's not possible to define words at arbitrary points? That's to keep the fiction that the programmer never actually defines words, but rather states equalities. The only odd part here is that while a real proof could have arbitrary text on either side of the equal sign, a Joy definition must always have a single word on the left side. Thus, a real equality could say: dup drop == id While in Joy you can only say id == dup drop Hmm, that might be an interesting extension ;-). Multiword '==' definitions would be the same as rewriting rules, which might be useful for optimizations. That would require a pretty sophisticated logic engine, though. At any rate, though, I agree completely with you that a real language would certainly have a modifiable system dictionary. Joy is more a proof of concept -- or even a tool to be used for proofs of a concept -- and as such it takes on certain restrictions which limit its usability. > > The definition of 'y' in Joy is: > > y == [dup cons] swap concat dup cons i; > Similarly, in my system, the Y combinator might be defined as: [...] > @@Define "Y" @@BM@@CBM > @@Define "M" @WI Good one. Thus I see that any definition in your language which includes a combinator without all of its 'inputs' applied is itself a combinator. This answers my question of how your users will be able to create combinators. > String literals would be a bit messier to unsugar if they were > more than one letter long (one would have to use a variation > of the "T" combinator, applying it to each of the character > primitives in turn, using currying). Don't bother -- sugar is good. If the phrase "syntactic sugar" has bad connotations, use "syntactic salt" instead. :-) > Of course, the factorial function could be written more concisely > using a function like Joy's "primrec" as: > Primrec (= 0) (K 1) Times > The parentheses could probably be omitted, the spacing giving > the structure: > Primrec Q0 K1 Times That's not a good idea -- spacing has no intrinsic superiority over parens, but parens make the grouping patterns a lot more flexible. Plus, using spacing would destroy the ability to use named combinators in those places. BTW, is the following correct? @@@Primrec @= 0 @K 1 Times > > > Hopefully you do not see a purely applicative approach as all > > > so bad now, seeing how similar in spirit it is to Joy. > > Well, I'm not sure that a single example can show that > > much. Can you refute > > my claim that Joy is simpler than even a truly pure > > applicative approach > > (and your language isn't pure, but rather adds the notion > > of combinators to an otherwise applicative base)? > Well, that depends on what precisely you mean by "simpler". > A purely applicative approach is simpler in the sense that > the only constructs needed are primitives and function-application, > while the Joy approach needs at least primitives, concatenation, > and quotation... You're ignoring your 'default' action, which is grouping -- what your language does when there's no @ sign. At the same time you're paying attention to my default action, concatenation. > You seem hesitant to accept that quotation is a real > construct of Joy... Why do you say that? I've listed it as such in every list I've given. The only time I acted suprised was when you claimed that it was "the fundamental" construct of Joy. It's clear that if any single concept has to be identified with Joy, it's concatenation. I've called it "function delimitation" in the list I gave. > In Joy, as the authors have formalized it, expressions are built > from primitives using two constructs: concatenation and quotation. > In purely applicative systems, only application is needed. > Parentheses often play the role of grouping that quotation does > in Joy, but parentheses can be eliminated as I've shown above. Very good point; I hadn't noticed that grouping was analogous to quotation. Of course, this is as much a statement about Joy as a statement about purely associative languages. It's clear that making concatenation explicit in a purely concatenative language would also result in allowing quotation to be implicit. It's a matter of choosing which one you want implicit. In your applicative language, you implicitly group and explicitly curry. In other applicative languages they explicitly group and implicitly curry. Thus application requires grouping. In Joy a programmer implicitly concatenates and explicitly quotes; making concatenation explicit would allow grouping to be implicit. As an example, the Y combinator would (possibly) be written thusly in an explicitly concatentive dialect of Joy: dup.cons swap.concat.dup.cons.i; I haven't worked out whether more complicated forms also work this way; I would have to do some thinking, and I don't feel like putting effort into something so completely silly ;-). I suspect that I'd have to use a prefix operator rather than an infix one. ..dup cons ....swap concat dup cons i; Sick. :-) It's interesting to note that by and large, quotation is rare in Joy (and Forth), but grouping is very common in the applicative languages I've seen. This makes the choice of which operation to make implicit easy in a concatenative language, whereas it's much harder with an applicative language. In some problems grouping is common enough to make your @ notation look quite attractive; in other problems, implicit currying is easier. Of course, there are counterexamples for every so-called general rule; have I simply mistaken the counterexamples for the rule? > > Furthermore, having looked at the proofs written in the Joy > > manuals, can you > > duplicate any of them as simply? > I believe so... I'm not sure exactly which proofs you're > talking about... > One proof that stands out was the "naturality of 'cons'". Here was the > proof they gave in the manual (in "The Algebra of Joy"): As you note, that's a poor proof on their part -- not a very good choice, unless your point was that the author of Joy has made mistakes. > Perhaps this was not such a good choice of thing to proof, since > it is not really analagous to the proof in the Joy manual; if you > have a favorite proof in the Joy manual, maybe I'll try that one. > Anyway, that was probably not exactly the cleanest way to do > it, either. Nice proof anyhow. Thanks. > Anyway, both the proof I just gave and the Joy proof are informal > proofs about systems... All proofs are informal from some point of view. You just have to match your assumptions up with your audience. At least, that's what my books say. > One interesting thing is that both proofs > used variables (in the "epi-language", not in the formal > language itself); > in the case of the purely applicative system at least, the proof could > be rewritten without variables at all using the Combinatory Axioms > (and abstracted versions of the Fst, Snd, and Nil axiom schemas). I postulate that the purely applicative system is equivalent to the concatenative system. I won't try to prove that, formally or otherwise. Thanks to your explanation I've clarified and refined my views. One of the reasons I thought concatenative logic was better was that I didn't know that applicative logic could handle unspecified variables; it's clear now that it can. It merely does so differently, by looking forward in the text of the function rather than back into the data of the program. At this point I'm going to go out on a limb again and try to make a distinction between the two systems: The applicative system requires full information about the complete "program" (I define a program as a complete set of functions and data) in order to run any part of it. The concatenative system does not require this much information; all we have to know is the portion of the program prior to the part you wish to execute. In other words, the applicative system is in line with the ideals of Tunes. The concatenative one doesn't contradict Tunes, but does allow execution without complete understanding. An applicative system only makes sense as a declarative system. A concatenative system also makes sense as an imperative one. I think this last feature is what attracts me to concatenative logic. Modern computers are by their nature imperative, and many of the problems solvable by them are well-described by imperative processes ("programs"). (Quantum computers, according to my understanding, are much more declarative.) Concatenation gives a formal, functional view which nonetheless comes closer to the actual processes taking place inside the computer. This gives concatenation a real edge, because it can also be used as a declarative language; it's all in which words you choose to run. But I may be speaking without complete understanding again ;-). > This would have been a bit trickier, though, and I'm not > really too familiar with the combinatory axioms, so I didn't > attempt it. To give you a flavor of how this might work though, > here are a couple combinatory axioms: > B(BB)B = C(BB(BBB))B It's going to take me a LONG time to see that as more than noise. (Not a problem -- consider that most concatenative systems have "stack noise".) > BCC = I This one makes sense, once I read below to recall what the single-letters stand for. (BTW, is it really _that_ important to use only a single letter for combinators? If so, there's a REAL advantage that concatenation posesses over application. ;-) converse converse == id. (boolean) not not == id. > The first one states the associativity of composition; Ah. In that case, would it be true that: concat concat == stack:(abc--cab) concat swap concat; expresses the same thought in a concatenative form? (Coincidentally, it expresses the rule both for lists and for quoted programs.) I find that to be easier to read; is that just me? Or am I just stating a special case of a more general proof? > for, from it it follows that > B(BB)B F G H = C(BB(BBB))B F G H > BB BF G H = BB(BBB)F B G H > B(BFG)H = B(BBBF) B G H > B(BFG)H = BBBF BG H > B(BFG)H = B BF BG H > B(BFG)H = BF(BGH) Sorry, I'm lost. I don't know most of the combinators, certainly not by their ISO standard one-letter names. I also forgot -- just for the moment -- the name of the Forth/Joy word which has the stack effect I wrote above (abc--cab); it's either 'rot' or '-rot', and rather than try to remember I simply wrote an explicit stack juggle. > or in other words, that > (F.G).H = F.(G.H) Ah, I get that! :-) > These two axioms are not particularly special, but with a > handful of ones like them, a sort of completeness follows; > it would be possible to directly show that This completeness is what you're talking about when you refer to a "finite" number of primitives, right? > Anyway, hopefully this example has helped you gain a feel for > the elegance a purely applicative system can have. Definitely has. Thank you again. > > I've recited already the five syntactic elements that every > > applicative > > language needs in order to produce code; I've also recited the three > > elements that a concatenative language needs. I've also > > made the claim that > > your language needs one additional element (combinators) > > which is distinct > > from any of the other five needed by a truly pure > > applicative language. > > Can you make any counterclaims? > This was mentioned above... I don't see that those five > "syntactic elements" are essential in every applicative language > or how they are evident in the one I've presented... They were certainly present in the language you _had_ presented. Four of them are still present in the language you've created for this post; Concatenative languages still only need three. > And, combinators are not particularly special elements in > the language I've given, but ordinary primitives. But not all of your 'ordinary primitives' are especially ordinary. It seems to me that you're bundling a lot of very different functions under the name of 'primitives', and claiming that because you can call them by a single name they're therefore simple. It seems to me that the compiler will have to be specially written in order to merely use many of the combinators. > Okay... Earlier I said that I'd give some reasons to like > purely applicative systems at the end of the post, so here are some > (these are reasons to prefer purely applicative systems over > similar systems that are mainly applicative, but with some extensions, > such as infix operators): > 1) The syntax is simpler. In a computing system, this may be > beneficial because it will make parsers, compilers, > and other programs that deal with expressions (such as > a program that transmits information between machines, > over networks) easier to construct. > (this is quite a minor one, compared to the next). Yes, although I suspect that a certain type of simplicity of syntax helps make proofs easier. So it's not _that_ minor. > 2) It ensures that the system can be made combinatorialy complete > with a finite set of combinators. If a system has an > infix operator that cannot be used as a first-class function, > then it is not generally possible to emulate functional > abstraction with combinators (rather, one would likely > need to resort to a lambda construct). True. Here's two reasons to prefer a concatenative system (this adds to your reasons): 3) Translation to machine code is simpler than translation of an applicative system. This is minor; Tunes doesn't want to use something simply because it's simpler. I personally do want that, but I'm influenced by Chuck Moore here. 4) The most commonly used metaprogramming systems currently are concatenative, although almost no languages now are concatenative. I'm talking about Postscript -- probably the most commonly used language (since it's used almost every time people print), and almost never used in any way other than metaprogrammed/machine generated. Thus, practical experience seems to indicate that people writing metaprograms are comfortable using concatenative languages. > So, what do you think of purely applicative systems now? I'm happier with your ideas than I was before -- but I'm still skeptical. You see, all the languages developed thus far (with the exception of Forth and Postscript) have been applicative languages, and I don't really like the results. But don't let my skepticism slow you -- one problem I have is an unwillingness to work on something which isn't new to me. Innovation is my goal, but it's not the only good goal -- you're working on refinement, and that's also a noble goal. > - "iepos" (Brent Kerby) -Billy From iepos@tunes.org Tue, 8 Feb 2000 13:37:11 -0800 (PST) Date: Tue, 8 Feb 2000 13:37:11 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Joy, Lambdas, Combinators, Procedures > > Still, it is not my view that non-purely-applicative systems > > are innately inferior... there may be other well-designed > > approaches that are also interesting (such as Joy's approach). > > My point is only that purely applicative languages are > > worth considering seriously; > The interesting thing to me about your language to me isn't really its > applicativeness; other languages are at least as applicative. The > interesting thing is the strong presence of combinators. Perhaps your > language would be better called a combinative language -- most people, when > discussing languages, assume applicative and lambda-based. Your language is > applicative and combinator-based; thus you say in one word 'combinator' the > essence of your difference from most modern language designs. Indeed, saying that my system is "combinator-based" would probably be the most concise way to distinguish it from traditional systems. I've used "purely applicative" here to distinguish my system from a Joy style ("concatenative") system, which is also based on combinators in a way. > > First of all, expressions that denote combinators (such as > > "B", "C", "W", and "K") are primitives in the language. > > Recall in my definition of "purely applicative" that > > primitives (meaning atomic expressions that are assigned > > precise meanings) are allowed, as long as there are only a > > finite number of them. In fact, primitives are essential > > in a purely applicative system (Joy also relies heavily > > on primitives (like "dup", "swap", "dip", "map", etc.), > > rather than fancy > > constructs; this is in contrast to languages like C and even > > Haskell, which rely on myriads of fancy constructs). > > WHOAH! Now I see what you meant. You really threw me off by using the word > "finite" there. (I don't know of any languages which use an infinite number > of primitives, by ANY definition of 'primitive'.) Yeah... I'm glad you see what I mean now... one thought that might be revelant: a system might like to formalize as primitive expressions the number literals 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... strictly, this is not allowed in a purely applicative system since it would require an infinite number of primitives. Instead, one could take as primitives only the numbers 0 through 9 (and maybe "ten"), and then refer to the other numbers using multiplication and addition. For instance, the number 532 could be referred to as (5 * ten * ten) + (3 * ten) + 2 Or without syntactic sugar for "+" and "*": + (* 5 (* ten ten)) (+ (* 3 ten) 2) > I suspect you'd dislike one of my potential changes to Joy: I wanted to > remove all of the stack manipulation words and replace them with a single > syntax convention: > > LET > dup == stack(a--aa); > drop == stack(a--); > swap == stack(ab--ba); > rot == stack(abc--bca); > 4reverse == stack(abcd--dcba) > IN > your code > END. Well, your "stack(--)" construct would then be much like a lambda construct of applicative systems (note that it requires the use of variables like "a", "b", and "c"). This would probably cause complications in proof systems. However, such a construct would still be quite interesting... It could be generalized so that arbitrary Joy expressions were permitted on the right of the "--"; then, for instance, "stack(ab -- b a foo a)" would be the program that takes two stack items, swaps them, and then runs the program "foo", but leaving an occurence of "a" on the stack; it could also be written as "dupd swap foo", I think. One interesting question to ask about Joy would be, "Could all uses of 'stack(--)' be eliminated using standard Joy programs like dup, pop, swap, and dip?" ... I'm guessing the answer is "yes"; I'd be curious to know of a specific algorithm that could be used to carry out such eliminations. > The number of 'primitives' is still finite, but is now not bounded by the > ones I choose (plus, I don't have to think of names). I'm not sure what you mean by this. There are an infinite number of expressions of the form "stack(...--...)", so you can't take them all as primitives (in the ordinary sense of "primitive"). Of course, you could just take "stack(--)" as a new special construct in addition to Joy's concatenation and quotation. > Anyhow, you have an interesting approach to 'primitives'; the way I see it, > each different type of primitive is a seperate category; I don't group them > all simply by calling them 'primitives'. It is true; most systems would need quite a few primitives, which one might want to group into categories when specifying the system. > Joy's primitives are all functions > (or all combinators, depending on how you view it; they're really the same > thing), and everything you define is also a function (and a combinator). Usually a distinction is made between a "function" and a "combinator": a function is a mapping from parameters to results, while a combinator is a special kind of function, a function that can be expressed using only closed lambda expressions (or, in Joy-style systems, a program that can be expressed using a "stack(--)" expression with nothing occuring on the right side except variables bound on the left; for instance, "dup", "swap", "pop", "rot"). The precise meaning of "combinator" may vary a bit among uses, but calling all functions "combinators" is getting a bit nonstandard. > Essentially, there's no distinction between primitive words and user words; Also in my system, I don't plan to make a sharp distinction between words that are "native" to the system (if there are such words) and words that have been defined meaning by the user. In general, I've meant to call all words "primitives" (by "words", meaning expressions that have no gramattical structure in the system), although I guess maybe I've deviated a bit from this terminology... > the only distinction which exists in the system is the distinction between > syntax and words. Joy has a lot of syntax; I personally suspect that I will > keep the quotation syntax, and I'll add the concept of immediate execution > and postponement, so that the coder can write words which act to modify the > compilation process, and then I'll throw in user-definable lexing (as shown > by the stack shufflers above). I'm not sure what you mean by "immediate execution" and "postponement"... > > Third, definitions... Actually, I don't recall using any sort > > of definition construct in my language, but it is something > > that will be needed eventually... In any case, I don't > > think a special construct is needed for definitions. > > It actually is special -- [...] > > But I'm getting ahead of myself. > > In this case, the special action of the Define primitive is modification of > the system dictionary, a dictionary which is specially distinguished because > all word lookups start with it, even though it has no name of its own. This is true. The system dictionary(s) would be a bit special in that they will need to be referred to by the system whenever it needs to interpret something the user has written. However, the fact that there may be more than one system dictionary (for different users) tends to make them seem less special. But anyhow, that definitions and system dictionary were "not special" was not my main point; my main point (which you have not contradicted) was that a special language construct is not needed for definitions. > -- you're disregarding the fact that any or all of > your 'primitives' could actually be 'special constructs'. I'm not quite sure what you mean by this... I usually do not call expressions "primitives" if they have a grammatical structure (i.e., if they are formed from parts that independently have meaning)... > You imply that by > calling a language feature a "primitive" and giving it a name, you evade any > complications it causes. I don't recall saying that; but, it is true that replacing a fancy language feature with a primitive or two can avoid some (but usually not all) complications associated with the feature. > Speaking of immediate execution, how would your system handle Forth-like > metaprogramming? Does the code have full access to all of the unparsed > source? This is an interesting question... Probably yes... As an example, if a user has been working on writing a complicated program, he would probably have given a name to a field for the program's text. Conceivably, this name could occur within the text itself, so that the program could refer to its own text cleanly. Of course, this is not to say that users will need to write complicated programs very often; in more cases than with traditional systems hopefully, a user would be able to get what he needs done by issuing a few commands to a shell, without resorting to a full-screen editor, and without naming his "program". On the other side of metaprogramming, languages in the system will probably provide ways of referring to the systems own low-level structures, including, for instance, procedures for peeking and poking at the system's raw memory, or doing raw I/O. Using these procedures, the system could change the way it works (it could even change itself so that it did not work at all). It would be hoped that the system's activity could be formalized well enough so that the system could safely change itself with these procedures. > > Anyway, definitions, in a way, are not really that a fundamental > > part of a system (at least, not the system I'm working on), > > although they are quite a visible part. Actually, > > They actually are fundamental -- if they weren't the compiler wouldn't know > how to look up the words you type. As I mentioned, the System Dictionary is > a distinguished element in language theory. Yes, a system dictionary would be very important to a system's shell, which needs it to look up the words users types, but the shell isn't really that fundamental a part of the system; not nearly so fundamental as, say, the system's "compiler", which translates the system's high-level procedures (expressed in an internal (nearly)-purely-applicative language) into low-level instructions, or the system's memory manager... Anyway, this isn't that important of a point (that definitions aren't an fundamental part of the system; incidentally, it may be argued that this sentence, that that point is not important, is not that important either). I concede that definitions are a fundamental part of languages and the system's shell(s), although not of the system as a whole. > > I'm really not sure what you mean by "function invocation" as > > distinguished from "function application", and I don't know > > what you mean by "run the function"... > > > By "function", I usually mean an abstract mapping from things to other > > things. The things that will be "ran" in the system will be > > procedures, not generally functions. > > This is not something you mentioned before. So your language distinguishes > between procedures and functions? How? Why? Doesn't that have negative > implications as far as proofs go? Wouldn't it make metaprogramming harder? This is quite a few questions... I'll see if I can explain... In my system, I might like to talk about the function of squaring (multiplying a number by itself); this is a function and not a procedure. By "procedure", I mean a specific command that something should be done, or the idea of that specific thing getting done. In this sense, here are some examples of some procedures (written in English): - Go home. - Reboot yourself. - Output "Hello World" to the user. These are procedures and not functions (except perhaps in the sense of a function taking another procedure as the next thing to do, yielding a completion, if one thinks of procedures that way). And, in this sense, the idea of "Output" (like the "printf" of "C") is not itself a procedure, but a function that takes a string and then yields a procedure. I view this as a crisp view of functions and procedures; I view the muddling of functions and procedures as evident in, say, the "C" language to be very irritating. It makes it impossible to refer to a procedure without the system actually carrying it out. Joy's setup is cleaner than C's of course; functions are emulated using procedures that pop and push things off the stack. A procedure could be talked about without being executed by using []s. But, in a purely applicative system, this setup seems unnatural; functions are primitive in the system (having connections with the system's application construct) and do not need to be emulated using procedures. An example may help illustrate... In a C-like system, the expression "square(7)" might represent a procedure to find the square of 7 (i.e., push the normal representation of such a number onto the stack); in my system, it would represent the square of 7 itself, or in other words, 49. > Is this distinction a necessary element of applicative languages of the type > you describe? It's not present in any way in any currently existing > concatenative language. It seems like quite a natural distinction to make; I don't know if it is necessary. Concatenative languages like Joy use procedures that are like functions, in that take pop things off the stack (like parameters), and push things back on. In the kind of language I describe, however, it is not necessary to talk about "the stack", and functions are thought of as abstract mappings (with a connection to the system's application construct), rather than programs that push and pop expressions off stacks. > > I'm not really sure what you mean by each of those syntactic elements > > and certainly don't see that they are required in all purely > > applicative systems. I've given the syntax of a small purely > > applicative language above, with these being some expressions in it: > > > @BW > > @@BCC > > @B@WB > > K > > @@B@BW@@BC@BB > > > How are your 5 syntactic elements manifest in this language? > > 1) Currying is explicit. > 2) Function delimitation is implied by a newline. > 3) Name definition is not present (but you cover it later, and you don't > deny that it's needed); note that in the examples you give, the functions > are simply thrown away, no possibility of reuse. > 4) Curry order modification is carried out by the absence of @ (as I > mentioned, you've only made grouping implicit). > 5) Parameter counts are needed to actually run/use any of your functions; > your procedural sublanguage will need to understand them even if your > applicative language doesn't recognise them. Also, your language will have > to keep count just in case the programmer tries to apply too many arguments > to a function. > > I'm seeing something I didn't notice up till now, though. I seem to see > that function delimitation isn't needed in either of our languages -- > grouping/quotation is sufficient (and grouping in your language carries out > curry order modification). This leaves your language with 4 outstanding > features and mine with 3; the missing element is parameter counts. I still do not quite get exactly what you mean by "parameter counts". > In your language parameter counts are very interesting and complex; a > parameter can be shunted through a _lot_ of combinators, some of them with a > runtime action (such as the 'select' combinator); and the end result has to > be known before the result can be run. I'm not sure whether this means that > runtime combinators are impossible for you; and if it does mean that, I'm > not sure whether that denies you completeness or makes the programmer's job > harder. Not sure what you mean by "select" combinator or "runtime" combinators. There is one issue that I think may be underlying a lot of your comments... In a Joy system, it is easy for an interpreter to tell what to do just by looking at an expression; for, an expression will consist of a sequence of atomic programs which the interpreter knows how to do (well, the sequence may also include quotations, which the interpreter can execute easily just by pushing the structure onto the stack). In contrast, in the type of system I'm describing, it may not always be easy for an interpreter to tell what to do just by looking at an expression. An example may help... consider the expression put (intToString 37) This expression would represent a procedure to output the string "17". Without sugar it might be written: put (intToString (+ (* 3 ten) 7)) There is not a one clear way that an interpreter should proceed in order to get this procedure executed. It might likely recognize that it is an expression of the form "put x"; thus it would like to know "x" well enough that it could actually output it. Yet, "intToString (+ (* 3 ten) 7)" is probably not immediately meaningful to the interpreter; thus, it will probably need to Rewrite it into a more Normal form. Exactly how this "normalization" will take place may vary from system to system. In more complicated examples, the issue of evaluation order might come up... a non-toy system will probably need to compile procedures in some way if they are used frequently. The point I should stress, I guess, is that a language (such as the purely applicative language I've been describing) is only a small part of the design of a computing system; the system will also need facilities for actually carrying out procedures written in the language; these facilities are quite important to consider when designing the system, although I neglected to say much about them; they may be a bit trickier to set up in a system that uses a purely applicative language than in a system that uses a concatenative one. Also, a system might conceivably use more than one language. My prototype system will probably use two languages initially: - one internal language that the system uses. An expression in this language will be a pointer to a C structure, with perhaps an "int" at the beginning indicating what primitive the expression represents, or a "0" if the expression is an application, in which case the following two fields (function and parameter) of the structure will be used. - one external langauge that the user uses. An expression in this language will be a sequence of characters. Both languages will be nearly purely-applicative, and they will probably be quite similar. > > Combinators in my purely applicative language are treated as ordinary > > primitives; they do not have special language support any more > > than Joy has special support for "dip", "swap", "dup", and "pop". > > Not true. Combinators in your language change the order of execution; they > completely change the emitted code. You don't seem to see a distinction > between primitives which drive the compiler and primitives which simply have > runtime behavior. All of Joy's primitives have only runtime behavior; most > of your combinators require compile-time behavior. Again, my language provides a way to express abstract functions, procedures, and other things. "compile-time behavior" and "runtime behavior" really have nothing to do with the language. So, I reaffirm that combinator primitives are ordinary primitives and do not have special language support; however, it is true that they will probably need to be dealt with specially by an interpreter or compiler. > This is the problem I was hinting about earlier, by the way. You define > "primitive" such that it includes many very different words, including many > words which are actually syntactic in their effect. Not sure what you mean by this... perhaps you are referring to the way I referred to the "S3" combinator (as "S3"), which has a sort of structure (as one might also want to refer to "S4" and so forth). In a pure system I would probably not call "S3" a primitive, but would probably rather take as primitive the series of S's. We might call this series "So", and then "S3" could be referred to as "So 3" (the application of "So" to "3"; by "series" we mean a function taking a natural number). > So although Joy's primitives all include compiled support, none of them > require compile-time behavior. They all do only one job: juggle the stack > at runtime. Well, some Joy primitives do other things than juggle the stack (such as "put", "get", and "gc"), but I see your main point, that every Joy primitive represents a program that can be easily compiled, while the same is not true for my system. > If someone invents a new combinator which nobody in the past > ever imagined, it'll be a simple thing to add it to Joy (witness my little > stack-shuffler, which essentially allows the trivial definition of new > combinators) "my little stack-shuffler", i.e., the dreaded lambda construct in disguise ;-) > but to add it to a syntax-based language will be a headache. I'm not sure what you mean by this. I guess by "syntax-based language" you mean a non-concatenative one (for instance, my system); I don't see how it would be such a headache to add a new combinator in my type of system... all combinators can be constructed from just a few (for instance, "B", "C", "W", and "K"), so if the system could handle procedures written using those few combinators, then it could handle procedures written using other ones, by just replacing occurences of the new ones by their definitions in terms of the old ones. > > Interestingly, Joy has a special > > construct for definitions, which could have been avoided... > > For example, one might define "popd" as "[dip] pop" in Joy by: > > > popd == [dip] pop > > > Yet this could have achieved without treating "==" specially, > > if they had added a "define" primitive, in this sort of way: > > > "popd" [[dip] pop] define > > > "define" could be treated as an atomic program (that has an effect > > other than on the stack, like "put" and "get") that modifies > > the interpreter's namespace... Of course, one would still like > > to have "==" as sugar. This is the approach I plan on using for > > definitions in my system. > > But in Joy "==" is more than sugar -- it's a fundamental part of the > notation. The model of Joy is that functions take a single parameter (the > stack) and return a single parameter (another stack); there's not supposed > to be a special dictionary available to them; the dictionary is supposed to > be immutable. The '==' syntax is intended to make Joy programs look like > proofs; notice how the language syntax is set up so that it's not possible > to define words at arbitrary points? That's to keep the fiction that the > programmer never actually defines words, but rather states equalities. > > The only odd part here is that while a real proof could have arbitrary text > on either side of the equal sign, a Joy definition must always have a single > word on the left side. Thus, a real equality could say: > > dup drop == id > > While in Joy you can only say > > id == dup drop Yes, the "==" construct in Joy is really only a definition construct, not a general equality. > Hmm, that might be an interesting extension ;-). Yeah, there are several ways the identity combinator can be defined in terms of other combinators, for instance I = WK I = SKK I = CKK This probably doesn't make sense... It's not too important though, so I won't elaborate unless you ask... > Good one. Thus I see that any definition in your language which includes a > combinator without all of its 'inputs' applied is itself a combinator. Exactly... > > Of course, the factorial function could be written more concisely > > using a function like Joy's "primrec" as: > > > Primrec (= 0) (K 1) Times > > > The parentheses could probably be omitted, the spacing giving > > the structure: > > > Primrec Q0 K1 Times > > That's not a good idea -- spacing has no intrinsic superiority over parens, > but parens make the grouping patterns a lot more flexible. Plus, using > spacing would destroy the ability to use named combinators in those places. Hmm... well, I'll avoid using that kind of notation then... > BTW, is the following correct? > > @@@Primrec @= 0 @K 1 Times Yes... > > Well, that depends on what precisely you mean by "simpler". > > A purely applicative approach is simpler in the sense that > > the only constructs needed are primitives and function-application, > > while the Joy approach needs at least primitives, concatenation, > > and quotation... > > You're ignoring your 'default' action, which is grouping -- what your > language does when there's no @ sign. At the same time you're paying > attention to my default action, concatenation. I'm not sure what you mean by "grouping" in a system based on "@". There is no "grouping" construct... The only construct is application. there is no default, no action. this is only a language. but, maybe I've misunderstood you. > > You seem hesitant to accept that quotation is a real > > construct of Joy... > > Why do you say that? I've listed it as such in every list I've given. The > only time I acted suprised was when you claimed that it was "the > fundamental" construct of Joy. It's clear that if any single concept has to > be identified with Joy, it's concatenation. I guess maybe I misinterpreted what you said... anyway, it is true that concatenation is Joy's most fundamental construct. One I said earlier was motivated by the thought that Joy's quotation and concatenation constructs could be replaced by a single construct, a list construct. This was the construct which I mentioned was most fundamental to Joy, although that did not make sense, because the author did not formalize Joy using a list construct at all. > > In Joy, as the authors have formalized it, expressions are built > > from primitives using two constructs: concatenation and quotation. > > In purely applicative systems, only application is needed. > > Parentheses often play the role of grouping that quotation does > > in Joy, but parentheses can be eliminated as I've shown above. > > Very good point; I hadn't noticed that grouping was analogous to quotation. > > Of course, this is as much a statement about Joy as a statement about purely > associative languages. It's clear that making concatenation explicit in a > purely concatenative language would also result in allowing quotation to be > implicit. > > In your applicative language, you implicitly group and explicitly curry. In > other applicative languages they explicitly group and implicitly curry. > Thus application requires grouping. In Joy a programmer implicitly > concatenates and explicitly quotes; making concatenation explicit would > allow grouping to be implicit. > > As an example, the Y combinator would (possibly) be written thusly in an > explicitly concatentive dialect of Joy: > > dup.cons swap.concat.dup.cons.i; > > I haven't worked out whether more complicated forms also work this way; > would have to do some thinking, and I don't feel like putting effort into > something so completely silly ;-). I suspect that I'd have to use a prefix > operator rather than an infix one. I don't think it would work. How would you represent "[]" or "[dup]"? But, then again, it would be completely silly, so it doesn't really matter. > It's interesting to note that by and large, quotation is rare in Joy (and > Forth), but grouping is very common in the applicative languages I've seen. > This makes the choice of which operation to make implicit easy in a > concatenative language, whereas it's much harder with an applicative > language. In some problems grouping is common enough to make your @ > notation look quite attractive; in other problems, implicit currying is > easier. Yes... this is all about external syntax, of course. Internally, a system would probably use a structure of two pointers (to other structures) to represent application. > Of course, there are counterexamples for every so-called general rule; have > I simply mistaken the counterexamples for the rule? I'm not sure... I would guess that the approach with "implicit currying" would probably turn out to be the most convenient, especially if parentheses were supplemented with separators like ";" and "," so that f; g, h x; z is shorthand for f (g (h x)) z Of course, internally, a system will probably not use either "@" or implicit currying, but a binary tree of pointers. > > > Furthermore, having looked at the proofs written in the Joy > > > manuals, can you > > > duplicate any of them as simply? > > > I believe so... I'm not sure exactly which proofs you're > > talking about... > > One proof that stands out was the "naturality of 'cons'". Here was the > > proof they gave in the manual (in "The Algebra of Joy"): > > As you note, that's a poor proof on their part -- not a very good choice, > unless your point was that the author of Joy has made mistakes. That was not my intent... :-) > > Anyway, both the proof I just gave and the Joy proof are informal > > proofs about systems... > > All proofs are informal from some point of view. You just have to match > your assumptions up with your audience. At least, that's what my books say. Well, there are such things as formal proof systems, with rigorous syntactic axioms and inference rules. By saying that my proof was "informal", I meant to ensure you knew that I was not using such a system (because it could almost look like I was, the way I presented it), so that you would not pick at my use of "..." or anything :-) (of course, what you said, that "proofs are informal from some point of view", I'll agree with. "Formal proofs" that result from textual inference systems are not really "proofs" at all in the psychological sense of the word that I think you were using). > I postulate that the purely applicative system is equivalent to the > concatenative system. I won't try to prove that, formally or otherwise. I also suspect that the two approachs may be equivalent, meaning that every Joy-style concatenative system has an associated purely applicative system with analagous primitives and vice versa; I'm probably not familiar enough with Joy to figure the exact relationship out... it is surely more complex than just "flip the expression around backwards and change []s to ()s". this does not quite cut it... Joy requires []s in cases where parentheses are not necessary in purely-applicative systems. For instance, "[foo] y" might could be written as just "Y foo". Joy seems to require functions to be quoted when they are used as parameters to other functions, while this is not the case in purely-applicative systems. On the other hand, purely-applicative systems require parentheses in some cases that Joy does not need []s. For instance, the expression "+ (* 2 3) (* 3 4)" can be written in Joy as "4 3 * 3 2 * +". > Thanks to your explanation I've clarified and refined my views. One of the > reasons I thought concatenative logic was better was that I didn't know that > applicative logic could handle unspecified variables; it's clear now that it > can. It merely does so differently, by looking forward in the text of the > function rather than back into the data of the program. Similarly, before I heard of Joy, I hadn't realized that a imperative stack-based approach could entirely eliminate variables. Also, thanks for your comments... I've come to see Joy and similar systems as worth considering (more than as just above-average imperative flukes). > At this point I'm going to go out on a limb again and try to make a > distinction between the two systems: > > The applicative system requires full information about the complete > "program" (I define a program as a complete set of functions and data) in > order to run any part of it. Not really... a purely-applicative system could conceivably execute the first part of a procedure without examining the trailing part; I plan on making my prototype system work this way. It may need to _parse_ the whole textual program before beginning execution, but it will not need to normalize the whole thing. > The concatenative system does not require this much information; all we have > to know is the portion of the program prior to the part you wish to execute. > > In other words, the applicative system is in line with the ideals of Tunes. > The concatenative one doesn't contradict Tunes, but does allow execution > without complete understanding. It seems to me that both systems could be used to execute programs without complete understanding of them. > An applicative system only makes sense as a declarative system. A > concatenative system also makes sense as an imperative one. I'm not sure what precisely you mean by "declarative" and "imperative", but this may be an apt description. An applicative system like the one I've described can be used to represent all kinds of things, including numbers, functions, and procedures. A Joy-style system can only be used to represent procedures, but procedures that push expressions representing, say, numbers onto the stack can be used to emulate numbers. > I think this last feature is what attracts me to concatenative logic. > Modern computers are by their nature imperative, and many of the problems > solvable by them are well-described by imperative processes ("programs"). I think you have again touched on one of Joy-style systems' best advantages: they are more straightforwardly executed on modern machines. I do not doubt this; but, I think programs expressed in purely-applicative languages may be easier to reason about (reasons for this will be given at the end of the post). With a nice metasystem, however, I suspect that programs expressed with a purely-applicative language could be executed just as efficiently as Joy-style programs. Likewise, with a bit of overhead, I suspect that one could reason well about Joy-style programs. So, the question of which system is superior is a matter of taste... Joy programs are more straightforwardly executed but less straightforwardly understood. But, maybe this is a naive conclusion. > > This would have been a bit trickier, though, and I'm not > > really too familiar with the combinatory axioms, so I didn't > > attempt it. To give you a flavor of how this might work though, > > here are a couple combinatory axioms: > > > B(BB)B = C(BB(BBB))B > > It's going to take me a LONG time to see that as more than noise. (Not a > problem -- consider that most concatenative systems have "stack noise".) > > > BCC = I > > This one makes sense, once I read below to recall what the single-letters > stand for. (BTW, is it really _that_ important to use only a single letter > for combinators? If so, there's a REAL advantage that concatenation > posesses over application. ;-) > > converse converse == id. > (boolean) not not == id. > > > The first one states the associativity of composition; > > Ah. In that case, would it be true that: > > concat concat == stack:(abc--cab) concat swap concat; > > expresses the same thought in a concatenative form? Yes... this states the associativity of "concat". This is similar to composition, I suppose. > I find that to be easier to read; is that just me? Well... You have cheated by using "stack:(abc--cab)". (although I see your note below now that Joy has a primitive for it). The combinatory axiom could be made more readable if one used combinators other than "B" and "C" to express it; for instance, foldl 2 B = foldr 2 B expresses the same thought, where "foldl" and "foldr" are a certain series of combinators, such that "foldl 2" and "foldr 2" have these properties: foldl 2 f x y z = f(fxy)z foldr 2 f x y z = fx(fyz) > > for, from it it follows that > > > B(BB)B F G H = C(BB(BBB))B F G H > > BB BF G H = BB(BBB)F B G H > > B(BFG)H = B(BBBF) B G H > > B(BFG)H = BBBF BG H > > B(BFG)H = B BF BG H > > B(BFG)H = BF(BGH) > > Sorry, I'm lost. I don't know most of the combinators, certainly not by > their ISO standard one-letter names. Hmm... perhaps my spacing was confusing... anyway, the only combinators I used were "B" and "C": B's rewrite rule: Bfgx = f(gx) C's rewrite rule: Cfxy = fyx The "F", "G", and "H" were variables... I'll use "x", "y", and "z" this time... Remember that "B" acts as a function that splits a functions first parameter, so to speak, into two (function and parameter). Applied to two functions, it yields their composition. "C" takes a function's converse. First see that "B(BB)B" is a function that takes three parameters, "x", "y", and "z", yielding "(x . y) . z": B(BB)Bxyz = (B (B B) B x) y z [just introduced extra ()s and spaces] ^ = ((B B) (B x)) y z [by B's rewrite rule, on the "^"ed "B"] = (B B (B x)) y z [removed extra ()s] = B B (B x) y z [removed more extra ()s] = (B B (B x) y) z [added extra ()s] ^ = (B (B x y)) z [by B's rewrite rule, on the "^"ed "B"] = B (B x y) z [removed extra ()s] = (x . y) . z Now see that "C(BB(BBB))B" is a function that takes three parameters, "x", "y", and "z", yielding "x . (y . z)": C(BB(BBB))Bxyz = (C (B B (B B B)) B x) y z [added extra ()s and spaces] ^ = (B B (B B B) x B) B y z [C's rewrite rule] ^ = (B (B B B x) B) y z [B's rewrite rule] ^ = B B B x (B y) z [B's rewrite rule] ^ = B (B x) (B y) z [B's rewrite rule] ^ = B x (B y z) [B's rewrite rule] = x . (y . z) Now do you see how from "B(BB)B = C(BB(BBB))B" it follows that "(x.y).z = x.(y.z)" for all "x", "y", and "z"? > > These two axioms are not particularly special, but with a > > handful of ones like them, a sort of completeness follows; > > it would be possible to directly show that > > This completeness is what you're talking about when you refer to a "finite" > number of primitives, right? I'm not sure how this completeness has to do with "finite number of primitives", except that the completeness could be established with a finite number of axioms. The precise completeness I'm talking about is the so-called "principle of extensionality" which says that if "Fx = Gx" is provable for an arbitary "x" in a system, then "F = G" is also provable; in other words, if two functions behave the same way, then they are the same. It would be possible to explicitly postulate the principle of extensionality, of course. Using it, one could show that "f.(g.h)" is the same as "(f.g).h" by arguing that both, when applied to an "x", give "f(g(hx))". > > > I've recited already the five syntactic elements that every > > > applicative > > > language needs in order to produce code; I've also recited the three > > > elements that a concatenative language needs. I've also > > > made the claim that > > > your language needs one additional element (combinators) > > > which is distinct > > > from any of the other five needed by a truly pure > > > applicative language. > > > > Can you make any counterclaims? > > > This was mentioned above... I don't see that those five > > "syntactic elements" are essential in every applicative language > > or how they are evident in the one I've presented... > > They were certainly present in the language you _had_ presented. Four of > them are still present in the language you've created for this post; > Concatenative languages still only need three. Let's see... here were the four you listed: > 1) Currying is explicit. > 3) Name definition is not present (but you cover it later, and you don't > deny that it's needed); note that in the examples you give, the functions > are simply thrown away, no possibility of reuse. > 4) Curry order modification is carried out by the absence of @ (as I > mentioned, you've only made grouping implicit). > 5) Parameter counts are needed to actually run/use any of your functions; This does not really make sense to me. These seem to be just observations about my system, rather than "syntactic elements". In particular, how can #3, "Name definition is _not_ present" (emphasis added) be a syntactic element? > > And, combinators are not particularly special elements in > > the language I've given, but ordinary primitives. > > But not all of your 'ordinary primitives' are especially ordinary. It seems > to me that you're bundling a lot of very different functions under the name > of 'primitives', and claiming that because you can call them by a single > name they're therefore simple. I do bundle a lot of very different functions under the name of 'primitives', but I have not claimed that they are simple just because they can be called by a simple names. Although I usually aim to take only fairly simple things as primitive, it would be possible to take complicated things as primitives (Fare calls this "abstraction inversion"). Anyway, what I said (and which you have not contradicted) was that combinators "are not particularly special elements in the language", meaning that that the language does not have complicated constructs for referring to them. However, even though the language does not have special constructs for dealing with them, they might be treated quite specially by the execution system; in this sense, as you have said, they are not so "ordinary". > It seems to me that the compiler will have to be specially written in order > to merely use many of the combinators. Exactly. Of course, the Joy interpreter also needed special support for its combinators "swap", "dup", "dip", and such. As you've commented, the only support it needed to give them is the ability to execute them (as atomic programs). My system might require more elaborate support for the analagous combinators; but, then again, perhaps it won't. > Here's two reasons to prefer a concatenative system (this adds to your > reasons): > > 3) Translation to machine code is simpler than translation of an > applicative system. This is minor; Tunes doesn't want to use something > simply because it's simpler. I personally do want that, but I'm influenced > by Chuck Moore here. This is a good point, and may be a true advantage of Joy-style systems. But, I suspect that there may be clever ways to compile procedures written as purely-applicative expressions. > 4) The most commonly used metaprogramming systems currently are > concatenative, although almost no languages now are concatenative. I'm > talking about Postscript -- probably the most commonly used language (since > it's used almost every time people print), and almost never used in any way > other than metaprogrammed/machine generated. Thus, practical experience > seems to indicate that people writing metaprograms are comfortable using > concatenative languages. Well... I don't really know much about Postscript, but I don't see how purely-applicative systems would have particular difficulty with metaprogramming. Of course, I don't know of any practical purely-applicative systems with flexible metaprogramming, since I don't know of any practical purely-applicative systems at all. > > So, what do you think of purely applicative systems now? > > I'm happier with your ideas than I was before -- but I'm still skeptical. > You see, all the languages developed thus far (with the exception of Forth > and Postscript) have been applicative languages, and I don't really like the > results. > > But don't let my skepticism slow you -- one problem I have is an > unwillingness to work on something which isn't new to me. Innovation is my > goal, but it's not the only good goal -- you're working on refinement, and > that's also a noble goal. I guess it could be called "refinement", although there aren't really any particular systems that I'm starting with to refine. Anyway, this kind of system does seem like an interesting thing to work on, whether it is new or not... > > - "iepos" (Brent Kerby) > > -Billy Let's see... I mentioned that I'd give some reasons why programs written as purely-applicative expressions might be easier to reason about than similar ones written in a Joy-style system, so now I'll give a couple: 1) In a purely-applicative system of the kind I've described, it is always possible to replace a sub-expression with one of equivalent meaning, while this is not the case in the Joy system, because of Joy's heavy dependence on a quotation construct. To illustrate, suppose in Joy one had a program [0 +] foo where "[0 +]" is used as a quoted program by "foo". Upon realizing that "0 +" is a program that doesn't do anything and is the same as "id", one might like to simplify the previous program to [] foo However, this change cannot be made without restriction; one much first check to make sure that "foo" does not use the program as a list (if it does, then the change may alter the behavior of the program). Such a complication does not occur in an analagous purely-applicative system. The procedure foo (plus 0) could be freely rewritten as foo I regardless of the structure of "foo", if the system had realized that the expression "plus 0" means the same thing as "I". This problem with Joy was mentioned in the manual (in the section "A rewriting system for Joy"); they presented some possible remedies, but none which they really liked. Anyhow, this is not a fatal problem of Joy, only a slight impediment to reasoning about programs (since often in reasoning about programs, one might like to rewrite a program by replacing a sub-expression with one that is known to have equivalent meaning, but this does not always preserve the meaning of the whole program in Joy). One thing to note is that such replacement works fine as long as the sub-expression to replace is not enclosed by []s. For instance, it would be okay to replace 2 3 + foo by 5 foo regardless of the structure of "foo". 2) Many fundamental equivalences of Joy programs that one might like to show require restrictions, while similar equivalences of purely-applicative expressions can be taken unrestrained. Specifically, one might like to think upon seeing a Joy program of the form x y swap that it could be rewritten y x But, this will not hold, for instance, if "x" is "2" and "y" is "sign". For 2 sign swap a program that inserts "1" under the top item of the stack, while sign 2 is a program that takes a number of the stack, replaces it with its sign, and then pushes "2". The restriction that one must take (and they mentioned this in the manual also) is that x y swap can only be rewritten as "y x" if "x" and "y" are programs that push one item onto the stack but have no other effects. In the last example, "sign" does not satisfy this, since it also pops an item off the stack. That this restriction is necessary seems to be quite a stumbling block for systems reasoning on Joy programs; it is not always immediately visible, upon looking at a program, how many items it pushes onto the stack, and whether it has any other effects. The analagous equivalence in a purely-applicative system is that @@@C f x y can be rewritten as @@f y x This can be accepted without restraint for all expressions "f", "x", and "y" of the system. A similar problem occurs with the "dup" and "pop" of Joy, but not with the analagous applicative combinators "W" and "K". These two problems are fairly serious, it seems, and make it more difficult for reasoning systems (and people as well, I would suppose) to understand Joy programs. They both seem to be fairly fundamental to the imperative approach, and cannot, to my knowledge, be easily remedied. However, similar purely-applicative systems seem to be free of these problems... Of course, purely-applicative systems do have problems of their own, particularly in the implementation, as was mentioned earlier... But, overall, the purely applicative approach still seems to me to be the best way to found a system. But, if you can see any awful problems with the approach, I'd still like to hear, of course. Of course, just saying that my approach is to use a purely applicative language based on combinators does not really say too much about how the system will work; there are many other important aspects of how a system might work which I've not really thought much about. (at this point, I'm going to begin rambling on some thoughts about *implementing* the system) One particular issue which needs to be decided is: "How will expressions be normalized so that the interpreter can understand them?" In my prototype system, I generally plan to represent expressions using a binary tree; the nodes will be scattered all over memory, and the parent nodes (i.e., expressions representing an application) will have pointers to the two children. Before it can understand an expression, the interpreter will in almost all cases need to rewrite it so that there are no combinator redexes (i.e., an expression with a combinator applied to enough parameters that a "beta-reduction" is possible) within. So, one thing that the interpreter might do first when interpreting an expression and reduce all its combinator redexes (i.e., change things like "Bfgx" into "f(gx)"), at least the ones that are at the head of the expression; sometimes, this process may never end; if this happens, it is the programmer's fault. (:-)) However, sometimes this kind of rewrite is not enough. For instance, if the interpreter sees an expression like "7 + 7", it would probably not immediately understand it, even though it contains no combinator redexes, and would need to normalize it to "4 + ten", or more likely to something like "2^1 + (2^(1+1)) + (2^(1+1+1))". This next thing was mentioned, I think, in my original post... I've planned having the prototype system use ncurses for its output and input, probably IRC style (with a one-line buffer at the bottom for input). However, rather than build in the stuff for input and output, I thought it would be an interesting test for the system to provide the high-level system with a few raw primitives for input and output and see if it could manage its own buffering, parsing, scrolling, and such. And of course, the first hurdle I've come to (and this is rather silly) is in making the interpreter execute the raw primitive for writing a character to the screen, which requires three parameters (two integer coordinates, and an integer telling what character to place). The interpreter needs to be able to take the three expressions representing the numbers and extract 32-bit ints from them, so that it can actually put the character on the screen. This really would not be such a difficult task, if the numbers were formed from, say, just "0" and "1", and addition, subtraction, multiplication, division, and exponentiation. It would be possible to rewrite each number expression into a base 2 format without too much trouble (one requirement is that the result be correct, even if an intermediate result might have overflowed 32 bits; so, the intermediate results will generally need to be stored in a more flexible form than "int", and more than int additions and multiplication will be needed). I still have not implemented that yet. But, this has started me thinking about some other issues... In the future, I'd like for the system to be able to handle negative numbers and fractional numbers (and possibly "imaginary"). On a high level, it should be possible to refer to these numbers in a uniform way; i.e., the functions "+", "*", should be meaningful when applied to all kinds of numbers, regardless of how they are represented. And, it would be nice if numbers like the square root of two and pi could be precisely represented. One important thing about a system of normal form for numbers is that one must be able to easily tell which of two numbers are larger (or if they are same), just by looking at the normal representation of each number. Here are a couple ways one might represent numbers: 1) Using a floating-point type representation. The advantage of this is, of course, that one would be able to do rapid calculations using them since most processors have special support. The downside is that there is the possibility of Error. Error may be acceptable in some cases in my system, as long as it can be precisely formalized and proven not to exceed a certain level. 2) A rational number could be represented precisely using a bit for sign and two integers, one for numerator and the other for divisor (the two integers being represented in some other format, probably the ordinary base 2). The downside (other than that it would be a bit slow) is that it is still not possible to precisely represent numbers like the square root of two and pi. Of course, there are probably other better ways of doing it that I don't know of. One thing that I've considered (this was mentioned in my original post) was using the Church Numerals (a very interesting class of combinators that are very similar to numbers; let me know if you are not familiar with them), but with an extension for negative numbers. In fact, I'm pretty sure that the church numerals would be nice to have at a high level; but I'm not sure that it will be any easier to invent a normal-form system for church numerals than for numbers in the ordinary sense. But, then again, maybe using church numbers would give some insight... Another aspect of normalization... the system, given two expressions, generally needs to be able to determine if the denote the same thing. In particular, the system will have an equality primitive "Q", and it needs to be able to reduce expressions like "Q x y" to a normal form, namely to an expression like "True" or an expression like "False" (actually, the system will probably use the combinators "K" and "KI" in place of propositions; these combinators have the property that "(K)xy" is "x" and "(KI)xy" is "y"; thus, they are their own if-then-else construct, so to speak). The obvious approach to normalizing "Q x y" to a proposition is to normalize "x" and then normalize "y", and then see if they have the same structure (if they do, then the whole thing normalizes to "True", otherwise, to "False"). This approach will only work if the system always normalizes expressions to the same normal form if they denote the same thing. Anyway, there would probably be use in some cases in the system keeping track of more than one name for the same thing (for instance, with numbers, some calculations may be easier if the number is in one form than if it is in another), although there does also seem to be some use in the system assigning every expression an associated "normal" expression (denoting the same thing), for the purpose of equality-testing... The issue of how the system will normalize things seems to be quite a complicated issue, and one that will grow in complexity as the system's language grows in complexity. Thus, it might be a good thing if the normalization system was a bit flexible... Anyway, this post has reached an absurd length... If you reply, feel free of course not to reply to every little comment, or don't reply to two comments in different places that say the same thing (I think I may have said the same things several times :-)) - "iepos" (Brent Kerby) From iepos@tunes.org Thu, 10 Feb 2000 08:08:28 -0800 (PST) Date: Thu, 10 Feb 2000 08:08:28 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Church numerals (was: Joy, Lambdas, ...) > I don't know of the Church Numerals, but they seem interesting. Perhaps you > could point in some directions? > > bineng The basic idea is this: a church numeral is a combinator that takes a function "f" and yields a function that applies "f" a certain number of times (how many times "f" is applied depending on the church numeral). For instance, the church numeral for 2 is a function that takes a function "f" and yields a function that applies "f" two times. Applying "Z2" (the church numeral for 2, as it sometimes called) to a "father" function would yield a "grandfather" function. Applying "Z3" to a "father" function would yield a "great-grandfather" function. Traditionally, church numerals are only used for natural numbers (0, 1, 2, etc.). But, it would make some sense to generalize them to all numbers... The church numeral for "-1" could be thought of as a function that given "f", yields a function that "takes away" an "f"; in other words, the church numeral for "-1" could be thought of as a function-inverter function. Similarly, the church numeral for "1/2" (one half) could be thought of as a function that given "f", yields a function that does "half" an "f"; doing such a half an "f" twice would be the same as doing a whole "f". For instance, applying the church numeral for "1/2" to the "grandfather" function would yield the "father" function. On the other hand, applying it to the "father" function would not really make any sense. Dealing with church numerals other than those for natural numbers can be a bit weird, since some functions have multiple inverses and "halves" or none at all... (as Fare has suggested, an inverter function could be an interesting way to found a system that has non-determinism; i.e., a system that can cleanly deal with expressions with multiple meanings or none at all). More information on church numerals could probably be found in most texts on lambda-calculus and such... There is a bit more information on my site (http://www.tunes.org/~iepos) under the section "Iterators". - "iepos" (Brent Kerby) From btanksley@hifn.com Thu, 10 Feb 2000 09:57:05 -0800 Date: Thu, 10 Feb 2000 09:57:05 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: FW: Joy, Lambdas, Combinators, Procedures The last email was not intended to go out. I have no idea why it was sent; one moment I was typing away, the next moment (right before my eyes) the email disappeared. I must have somehow hit some key which sent it, but I have no idea how. So wait before you reply -- it's neither complete nor proofread. You'll get another copy. Sorry about that. :-) -Billy -----Original Message----- From: Billy Tanksley Sent: Thursday, February 10, 2000 9:54 AM To: tunes@tunes.org Subject: RE: Joy, Lambdas, Combinators, Procedures This is a greatly fun discussion for me. If anyone on this list disagrees, please let me know and we'll take this into email. > From: iepos@tunes.org [mailto:iepos@tunes.org] > > > First of all, expressions that denote combinators (such as > > > "B", "C", "W", and "K") are primitives in the language. From btanksley@hifn.com Sat, 12 Feb 2000 14:16:19 -0800 Date: Sat, 12 Feb 2000 14:16:19 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: FW: Joy, Lambdas, Combinators, Procedures Okay, this (should be) the real copy. Unless I hit the wrong button again, in which case this message is simply your recommended daily allowance of irony. This is a greatly fun discussion for me. If anyone on this list disagrees, please let me know and we'll take this into email. Oh, I'm going to do a lot of clipping and summarizing, including the good examples/informal proofs you sent. I appreciate them, but there's no need for me to re-include them just because I like proofs. This won't reduce the size of the message enough, but there's only so much I can do about that. > From: iepos@tunes.org [mailto:iepos@tunes.org] > Yeah... I'm glad you see what I mean now... one thought that might > be revelant: a system might like to formalize as primitive expressions > the number literals 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... > strictly, this is not allowed in a purely applicative system since > it would require an infinite number of primitives. Instead, one > could take as primitives only the numbers 0 through 9 (and maybe > "ten"), and then refer to the other numbers using multiplication and > addition. For instance, the number 532 could be referred to as ...and with that I'll leave that idea behind, strongly hoping to never see it again. Sorry, but as much as I like the theory you're bringing to the table, I really, really hate messing up a good language with theory. Don't forget that the purpose of a programming language is to allow people to express things to the computer. Having a finite number of "primitives" is not required by any theory you've stated; it's simply an arbitrary restriction you've placed upon yourself. > > I suspect you'd dislike one of my potential changes to Joy: > > I wanted to > > remove all of the stack manipulation words and replace them > > with a single > > syntax convention: > > LET > > dup == stack(a--aa); > > drop == stack(a--); > > swap == stack(ab--ba); > > rot == stack(abc--bca); > > 4reverse == stack(abcd--dcba) > > IN > > your code > > END. > Well, your "stack(--)" construct would then be much like a > lambda construct > of applicative systems (note that it requires the use of variables > like "a", "b", and "c"). This would probably cause > complications in proof systems. Not at all, because the pseudovariables would not intermingle at all with functionality. It's nothing more than a new naming convention for a subset of the combinators, really. The 'C' combinator under this system has the name 'ba' or 'stack:(ab--ba)'. (In my original design and implementation under Pygmy Forth I accepted input of the simple form 'abc--cba' rather than requiring the 'stack:()' notation.) Do you see what I mean by "new naming convention"? These are nothing more than different names for the same old combinators. It's fortunate that I can write the combinator names in a way which resembles their actual operation; it's an implementation detail that none of the names are actually defined in the dictionary. > However, such a construct would still be quite interesting... > It could be generalized so that arbitrary Joy expressions were > permitted on the right of the "--"; then, for instance, > "stack(ab -- b a foo a)" would be the program that takes two > stack items, swaps them, and then runs the program "foo", > but leaving an occurence of "a" on the stack; > it could also be written as "dupd swap foo", I think. No, that WOULD be similar to a lambda construct. I would NOT want that thing in the core language. Of course, it might be in one of the extension libraries -- at times it would be quite useful. For example: concat concat == (abc--ab concat c concat) would express a proof we look at below without any stack juggling. Of course, it does the exact same thing as the stack juggle. > One interesting question to ask about Joy would be, > "Could all uses of 'stack(--)' be eliminated using standard > Joy programs like dup, pop, swap, and dip?" ... > I'm guessing the answer is "yes"; I'd be curious to > know of a specific algorithm that could be used to carry > out such eliminations The answer is yes; DIP in specific can be used to build any combinator, since it removes one element from the stack and holds it safe. (Forth doesn't have DIP, so Forth adds a return stack, which gives the same functionality.) If I ever get the time to write this compiler I'll write the algorithm to do this; it's pretty simple. Last time I tried I cheated by using assembly and hitting the registers directly (thus forcing a limit of 4 stack items per swap-op -- don't you love Intel?). > > The number of 'primitives' is still finite, but is now not > > bounded by the > > ones I choose (plus, I don't have to think of names). > I'm not sure what you mean by this. There are an infinite number > of expressions of the form "stack(...--...)", so you can't > take them all as primitives (in the ordinary sense of "primitive"). If it's impossible to have an infinite number of primitives, then why did you list "a finite number of primitives" in the list of features your language has? Seriously, though, 'primitives' is perhaps a little too general -- a 'primitive operation' is certainly possible to express in syntax. > Of course, you could just take "stack(--)" as a new special > construct in addition to Joy's concatenation and quotation. This is essentially what it is. As such it's no different from any of the sets of primitives in either of our languages. Except, of course, that this primitive, like all of the Joy words (and unlike Joy's quotations and definitions), adds no parsing effort outside of its own letters. > > Anyhow, you have an interesting approach to 'primitives'; > > the way I see it, > > each different type of primitive is a seperate category; I > > don't group them all simply by calling them 'primitives'. > It is true; most systems would need quite a few primitives, which > one might want to group into categories when specifying the system. The reason I'm calling your grouping into question is complexity; I believe that your system has very important categories which you're ignoring. For example, your primitive functions bear little resemblance in behavior to your primitive combinators. Your functions simply use their parameters; your combinators have to select and choose parameters. > > Joy's primitives are all functions > > (or all combinators, depending on how you view it; they're > > really the same > > thing), and everything you define is also a function (and a > > combinator). > Usually a distinction is made between a "function" and > a "combinator": a function is a mapping from parameters to results, > while a combinator is a special kind of function, a function > that can be expressed using only closed lambda expressions > (or, in Joy-style systems, a program that can be expressed > using a "stack(--)" expression with nothing occuring on the > right side except variables bound on the left; for instance, > "dup", "swap", "pop", "rot"). The precise meaning of "combinator" > may vary a bit among uses, but calling all functions "combinators" > is getting a bit nonstandard. This is practically true for applicative languages, but breaks down almost completely for concatenative ones. In such languages, ALL words (including so-called combinators and special forms) are actually functions from a stack to a stack. In addition, according to the foundational axioms of K-complexity theory, all functions can be built from a small set of specific combinators (I seem to recall S and K) combinators, so all functions can therefore be regarded as combinators in any system (including yours). If your system is going to use nolambda as its base language, then, I would have no quibble; everything for you would be a combinator (since nolambda appears to be based on K-complexity theory). However, odds are that you're going to be a bit more practical, and thus you're going to have a distinction between functions and combinators. > > Essentially, there's no distinction between primitive words > > and user words; > Also in my system, I don't plan to make a sharp distinction between > words that are "native" to the system (if there are such words) > and words that have been defined meaning by the user. In general, > I've meant to call all words "primitives" (by "words", meaning > expressions that have no gramattical structure in the system), > although I guess maybe I've deviated a bit from this terminology... I would simply call them "words". A word which is primitive to the system I would be tempted to call an "primitive word", although I have no innate objection to simply calling it a "primitive" (so long as the definition's explicit). > > suspect that I will > > keep the quotation syntax, and I'll add the concept of > > immediate execution > > and postponement, so that the coder can write words which > I'm not sure what you mean by "immediate execution" and > "postponement"... In Forth, words can be defined which act immediately even in the middle of compilation; for example, the Forth word POSTPONE acts immediately to scan ahead in the input stream, look up the next word, and compile its action into the current definition. (Note that I not only gave you the definition of 'immediate execution', I also gave you the definition of 'postponement'; it's called postponement because it can postpone the compile-time action of the next word until runtime.) This would allow me to include language modifications like: if [test] then [body] else [alternate]. (Note that in order to do this, 'if' has to scan ahead.) It will also allow me to avoid quotation, which will be useful for my idea of the LLL. I'll just do things the way Forth does them: you're not allowed to treat a definition as a list. This reduces expressive power, but with the macro ability provided by immediate execution and postponement this should matter not at all. > > > that will be needed eventually... In any case, I don't > > > think a special construct is needed for definitions. > > In this case, the special action of the Define primitive is > > modification of > > the system dictionary, a dictionary which is specially > > distinguished because > > all word lookups start with it, even though it has no name > > of its own. > This is true. The system dictionary(s) would be a bit special > in that they will need to be referred to by the system whenever > it needs to interpret something the user has written. However, > the fact that there may be more than one system dictionary > (for different users) tends to make them seem less special. Not true; the fact is still that all lookups start at the system dictionary, and allowing each user to have their own system dictionary doesn't change that at all, any more than giving them their own computer changes the fact that MacOS tends to crash ;-). > But anyhow, that definitions and system dictionary were "not > special" was > not my main point; my main point (which you have not contradicted) > was that a special language construct is not needed for definitions. I have contradicted that; although it's true that no special syntax is needed (an obvious point proven many times over), the primitives which access the system dictionary are in a special class of operations, a class which can't be proven using the same rules which apply to everything else: they take effect on a mutable structure which can affect every word in the following source (in a potentially very confusing way). In a very real sense it's impossible to 'reason' about a word "Define". > > -- you're disregarding the fact that any or all of > > your 'primitives' could actually be 'special constructs'. > I'm not quite sure what you mean by this... I usually do not call > expressions "primitives" if they have a grammatical structure > (i.e., if they are formed from parts that independently have > meaning)... A grammatical construct is a special construct, but a primitive word can have grammetical effect. In this case, most of the words you call combinators have grammatical effect, in that they affect the parsing of all the words which follow them. > > You imply that by > > calling a language feature a "primitive" and giving it a > > name, you evade any complications it causes. > I don't recall saying that; but, it is true that replacing a > fancy language > feature with a primitive or two can avoid some (but usually not all) > complications associated with the feature. I disagree with that. What you call primitives are in many cases themselves fancy language features. The fact that all of your combinators can be expressed in terms of a few simple combinators doesn't change the fact that all combinators -- especially including those few ones -- require special parsing from the compiler. > > Speaking of immediate execution, how would your system > > handle Forth-like > > metaprogramming? Does the code have full access to all of > > the unparsed > > source? > Probably yes... As an example, if a user has been working on writing > a complicated program, he would probably have given a name to > a field for the program's text. Conceivably, this name could > occur within the text itself, so that the program could refer > to its own text cleanly. This is not Forth-like; in fact, it's a superset of Forth's capabilities which will force you to re-parse the file every time it's used. That's pretty brutal. Forth's metaprogramming only parses text which the interpreter hasn't touched yet. I don't believe that this is even possible with your system; all text has to be parsed before anything can be evaluated. The nice thing is that it might be possible for you to manipulate the parsed structure at compile-time, given the capability to handle compile-time execution. This can be far more effective than text parsing, especially if you have any syntax in your language. I'm afraid that this implies procedural operation (because it implies 'execution' of code, something which isn't obviously functional), and you haven't covered how your system handles procedural operation yet. > Of course, this is not to say that users will need to write > complicated programs very often; in more cases than with > traditional systems hopefully, a user would be able to get what he > needs done by issuing a few commands to a shell, without resorting > to a full-screen editor, and without naming his "program". As with APL, I fully expect one-liners to be very common. However, as in APL, I expect most users to name their one-liners, and expect to use them later. APL has the concept of workspaces; everything you create in the interpreter is saved in the workspace and can be used at any time later. You might look at APL for inspiration (unfortunately, J doesn't use workspaces). http://juggle.gaertner.de/is the address of a good J page. I got there from dmoz.org. You have a good reason to suspect that you won't need Forth-style metaprogramming very often: Forth has no support for modifiable functions, and you have partial support. Your system doesn't currently have any obvious ways to reach inside a completely applied function and modify it, but you can play with the variables about to be passed to a function. (In this respect, Joy is clearly _much_ more expressive, since manipulating a quoted program as a list allows arbitrary modification.) > On the other side of metaprogramming, languages in the system will > probably provide ways of referring to the systems own > low-level structures, including, for instance, procedures for > peeking and poking at the system's raw memory, or doing raw > I/O. Using these procedures, the system could change the > way it works (it could even change itself so that it did not > work at all). That's (provably) always a potential 'feature'. :-) > It would be hoped that the system's activity could be formalized > well enough so that the system could safely change itself > with these procedures. I hope so, yes. Until the theory is worked out, it can be nice to have the raw capability, even if we know that someday it'll go away. > which needs it to look up the words users types, but the shell > isn't really that fundamental a part of the system; not nearly > so fundamental as, say, the system's "compiler", which Why would you claim that? I concede that your system can run without names, and thus without dictionaries of any kind, but any source with names of any kind is heavily affected by the system dictionary. If you provide _any_ primitives of _any_ kind which modify that dictionary, then you must acknowledge that those specific primitives are special. I have to agree with you that definitions don't _have_ to be included in either of our systems, though. It would be a bear to work with, but it would be *possible*. > > > things. The things that will be "ran" in the system will be > > > procedures, not generally functions. > > This is not something you mentioned before. So your > > language distinguishes > > between procedures and functions? How? Why? Doesn't that > > have negative > > implications as far as proofs go? Wouldn't it make > > metaprogramming harder? > This is quite a few questions... I'll see if I can explain... Grin. > In my system, I might like to talk about the function of squaring > (multiplying a number by itself); this is a function and > not a procedure. By "procedure", I mean a specific command > that something should be done, or the idea of that specific thing > getting done. In this sense, here are some examples of some procedures > (written in English): > - Go home. > - Reboot yourself. > - Output "Hello World" to the user. "Square a number." Okay, I'm with you. So 'procedure' connotes a process taking place in time, whereas a function could denote a step of that process, but doesn't state anything about time. > These are procedures and not functions (except perhaps in > the sense of a function taking another procedure as the next thing > to do, yielding a completion, if one thinks of procedures that way). I need to stop a moment and check something with you. So to make a function act procedurally, you need to transform it into a "completion" by attaching a next-thing-to-do to it, correct? That makes sense to me... Interesting. Okay, I see a problem. Not all procedures can be expressed as functions -- for example, printing to the screen. That's a pure procedure, with nothing functional about it. Let me study how you handle that (next paragraph)... > And, in this sense, the idea of "Output" (like the "printf" of "C") > is not itself a procedure, but a function that takes a string > and then yields a procedure. Okay, so it's possible to have computed procedures. This only makes sense. Let me try to write a brief program: & !@Output @stringify @@+ 4 5 stop (In this program, I'm using "!" as a prefix, dyadic operator which binds a "next action procedure" to a function, producing a completion; and "&" as a prefix, monadic operator which executes a single procedure. 'stop' is a predefined procedure which you've discussed before.) This doesn't work -- (@Output x) is a function, as required, but as a function it only acts to return a procedure. That's not useful right away; we want to _execute_ a procedure, not return one. Hmm. Perhaps you need a function/combinator which acts to execute a procedure. I'll call it "exec". & ! @exec @Output @stringify @@+ 4 5 stop Okay, that makes a little more sense. (I'm not allowing myself to think about the logic involved in a function which executes a procedure -- that doesn't even begin to make sense -- but it's the first thing I thought of.) However, in addition to that odd function, I've added two punctuation marks to your language. Thus, I must have gotten something wrong. > Joy's setup is cleaner than C's of course; functions are emulated > using procedures that pop and push things off the stack. A procedure > could be talked about without being executed by using []s. No, procedures and functions are the same in Joy; nothing's being emulated. Any source may freely be executed or reasoned about. You're confused by the fact that concatenative languages can be viewed freely as both procedural and functional; as a procedural language they have words which are procedures which take and return variable numbers of parameters, but as functional languages they have words which are functions and take and return a single parameter, an item of type "stack". Words in Joy are thought of in two ways: first, as procedures which pop and push things onto and off of a single system stack; and second, as functions which take a stack as a parameter and return a new stack as a result. In other words, Joy functions are also conceivable as abstract mappings. The difference is that Joy's functions can also be pictured as very efficient procedures, _with no semantic change_. That's very significant; in fact, it's a HUGE step closer to a complete formal view of the way computers actually operate, with each operation taking as input a "state" object and producing another "state" object as output. It's also mildly interesting that adding explicitly non-functional words to Joy does not destroy the functional character of the language; those words may be trivially discovered in any source text, and reasoning about them prevented or special-cased. They fit in well with the functional words, and the same reasoning can apply (so long as you don't carry out the non-functional aspect). > But, in a purely applicative system, this setup seems unnatural; > functions are primitive in the system (having connections with > the system's application construct) and do not need to be > emulated using procedures. Procedures, on the other hand, have to be "emulated" (whatever that means) using a complicated system of built-in operations, which are not functions. This, to use your own words, seems unnatural. > I still do not quite get exactly what you mean by "parameter counts". A function can't be 'executed' in any way I know of until all of its parameters are filled (otherwise its value is undefined). Thus you have to keep count of parameters. This is more complicated than it might seem; you can't simply trace through the code and keep a type stack, as you can with Joy; you have to parse the entire source and consider a potentially huge tree of parameters. For example, in your language, is this an error? @@@ackermann 4 5 6 (Ackermann is a two-parameter function.) > > In your language parameter counts are very interesting and > > complex; a > > parameter can be shunted through a _lot_ of combinators, > > some of them with a > > runtime action (such as the 'select' combinator); and the > > end result has to > > be known before the result can be run. I'm not sure > > whether this means that > > runtime combinators are impossible for you; and if it does > > mean that, I'm > > not sure whether that denies you completeness or makes the > > programmer's job > > harder. > Not sure what you mean by "select" combinator or "runtime" > combinators. A 'select' combinator is an example of a runtime combinator. Think of the Forth word 'PICK', which takes an integer "n" and pushes onto the stack the n-th item. Joy has no such combinator, although it would be easy enough to build one by using a loop and 'dip'. The point is that sometimes you don't know which item to take until runtime. > There is one issue that I think may be underlying a lot of your > comments... > In a Joy system, it is easy for an interpreter to tell > what to do just by looking at an expression; for, an expression > In contrast, in the type of system I'm describing, it may not always > be easy for an interpreter to tell what to do just by looking > at an expression. An example may help... consider the expression That was essentially my point -- not only that it's harder to "compile" this language, but also that it's harder to merely parse it. > > > Combinators in my purely applicative language are treated > > > as ordinary > > > primitives; they do not have special language support any more > > > than Joy has special support for "dip", "swap", "dup", and "pop". > > Not true. Combinators in your language change the order of > > execution; they > > completely change the emitted code. You don't seem to see > > a distinction > > between primitives which drive the compiler and primitives > > which simply have > > runtime behavior. All of Joy's primitives have only > > runtime behavior; most > > of your combinators require compile-time behavior. > Again, my language provides a way to express abstract functions, > procedures, and other things. "compile-time behavior" and > "runtime behavior" really have nothing to do with the language. > So, I reaffirm that combinator primitives are ordinary primitives > and do not have special language support; however, it is true > that they will probably need to be dealt with specially by an > interpreter or compiler. Runtime behavior may very well be irrelevant to your compiler (although I don't see the use of a language without runtime behavior); however, it's literally impossible that your language has no compile-time behavior. It has to at _least_ have the behavior of reading characters from the source file and redirecting them to /dev/null. From what you've described, it has a LOT more behavior than that. And my point is precisely that parsing your language is difficult. The main thing which makes it hard is your combinators, which in many cases have the effect of reordering the parse. > > So although Joy's primitives all include compiled support, > > none of them > > require compile-time behavior. They all do only one job: > > juggle the stack > > at runtime. > Well, some Joy primitives do other things than juggle the stack > (such as "put", "get", and "gc"), but I see your main point, that > every Joy primitive represents a program that can be easily > compiled, while the same is not true for my system. 'gc' is a no-op (in theory). 'put' and 'get' are not formalized in any way; however, considered simply as stack-effect machines, they're quite simple. The interesting thing is that they _can_ be considered that way; any "rewriting rule" (a misnomer which I'll discuss later) which applies to a function with the same stack effect will also apply to 'get' or 'put'. This is not true for the applicative language; it demands truly functional behavior. > > If someone invents a new combinator which nobody in the past > > ever imagined, it'll be a simple thing to add it to Joy > > (witness my little > > stack-shuffler, which essentially allows the trivial > > definition of new > > combinators) > "my little stack-shuffler", i.e., the dreaded lambda > construct in disguise ;-) Grin. I do intend to allow enough power to permit the user to add a lambda construct parser -- after all, as much as I despise lambda ;-), there are many problems where it's either the most appropriate solution or a very well-studied one. > > but to add it to a syntax-based language will be a headache. > I'm not sure what you mean by this. I guess by "syntax-based language" > you mean a non-concatenative one (for instance, my system); No -- I simply mean any language which requires the use of rewriting rules (or something even nastier, such as C's syntax) requires the compiler to know about that syntax. > I don't > see how it would be such a headache to add a new combinator in > my type of system... all combinators can be constructed from just > a few (for instance, "B", "C", "W", and "K"), so if the system could > handle procedures written using those few combinators, then it could > handle procedures written using other ones, by just replacing > occurences of the new ones by their definitions in terms of > the old ones. Look up K-complexity on the web -- it's the theory of reducing functions to just a few combinators. (Your compiler could theoretically work that way.) The problem is that it's a very hard task; also, you still have to have special support for the basic set of combinators, as well as descriptions of all the other combinators in terms of them. > > > while the Joy approach needs at least primitives, concatenation, > > > and quotation... > > You're ignoring your 'default' action, which is grouping -- > > what your > > language does when there's no @ sign. At the same time > > you're paying > > attention to my default action, concatenation. > I'm not sure what you mean by "grouping" in a system based on "@". > There is no "grouping" construct... The only construct is application. > there is no default, no action. this is only a language. but, > maybe I've misunderstood you. There _is_ an action -- your parser has to at least dump characters into /dev/null, as I've said before. There is an operator which your @ makes implicit; the action of this operator affects evaluation. The operator can be formally defined, although it's complicated enough that I'm not eager to do the definition. Okay, I'll take a try at it -- the blank space operator in your explicitly associative language has the operation of arranging its two operands on the same level of an initial parse tree, as children of the last application. This parse tree is temporary; combinators can rearrange it. The name of the operator, if it were made explicit, would be "sibling" or something like that. The default action in curry-based languages is easier to explain; it's currying, of course. > > As an example, the Y combinator would (possibly) be written > > thusly in an > > explicitly concatentive dialect of Joy: > > dup.cons swap.concat.dup.cons.i; > > I haven't worked out whether more complicated forms also > > work this way; > > would have to do some thinking, and I don't feel like > > putting effort into > > something so completely silly ;-). I suspect that I'd have > > to use a prefix > > operator rather than an infix one. > I don't think it would work. How would you represent "[]" or "[dup]"? > But, then again, it would be completely silly, so it doesn't really > matter. Good point. You'd have to have a single primitive with the effect of []. So let [] be 'nil', then [dup] would be '.dup nil'. Okay, now we're equivalent. > I'm not sure... I would guess that the approach with > "implicit currying" > would probably turn out to be the most convenient, especially > if parentheses were supplemented with separators like ";" and "," so > that > f; g, h x; z > is shorthand for > f (g (h x)) z I don't understand. To me the parentheses look clearer -- but I have no clue what your semicolons and commas are doing. > > As you note, that's a poor proof on their part -- not a > > very good choice, > > unless your point was that the author of Joy has made mistakes. > That was not my intent... :-) A good proof of that fact nonetheless ;-). I admit to skipping his proof when reading through the document; it turns out that the proof's complexity was indicative of problems. > > I postulate that the purely applicative system is equivalent to the > > concatenative system. I won't try to prove that, formally > > or otherwise. > I also suspect that the two approachs may be equivalent, meaning > that every Joy-style concatenative system has an associated > purely applicative system with analagous primitives and vice versa; That sounds viable. That's not exactly what I meant -- I simply meant that both notations can express the same set of functions. I'm not clear on whether they can both express the same set of _programs_, though; the fact is that concatentive programs can contain non-functions but still have functionally understandable characteristics. > Joy requires []s in cases where parentheses are not necessary in > purely-applicative systems. [] and () have little in common -- but the REAL difference is that the default operator in most applicative languages is curry, and the default in Joy is compose. It's the default actions that cause the most confusion. > Joy seems to require functions to > be quoted when they are used as parameters to other functions, > while this is not the case in purely-applicative systems. That's merely because the applicative systems don't _have_ quotation. Of course you don't require it; you can't. You have and require other things. > Also, thanks for your comments... I've come to see Joy and similar > systems as worth considering (more than as just above-average > imperative flukes). That's actually my experience with Joy as well -- before I met it I only knew that I liked Forth; I didn't know why. My interest had been in the more conventional functional languages and Forth; I didn't see a connection between the two (although I suspected one). > > At this point I'm going to go out on a limb again and try to make a > > distinction between the two systems: > > The applicative system requires full information about the complete > > "program" (I define a program as a complete set of > > functions and data) in > > order to run any part of it. > Not really... a purely-applicative system could conceivably execute > the first part of a procedure without examining the trailing part; > I plan on making my prototype system work this way. It may > need to _parse_ the whole textual program before beginning execution, > but it will not need to normalize the whole thing. The parse either consists of a complete normalization, OR the parse has full information about the operation of every combinator (in other words, it has to have enough information to normalize the whole thing). It certainly _will_ have to normalize all of the text before the portion being executed, and it'll also have to normalize any of the text afterwards which eventually gets used as an argument. The equivalent 'minimal parse' in Joy involves normalizing up to and including the part being executed. That's it. > > The concatenative system does not require this much > > information; all we have > > to know is the portion of the program prior to the part you > > wish to execute. > > In other words, the applicative system is in line with the > > ideals of Tunes. > > The concatenative one doesn't contradict Tunes, but does > > allow execution > > without complete understanding. > It seems to me that both systems could be used to execute programs > without complete understanding of them. Technically true, but the partial understanding needed to execute an applicative system is MUCH more in-depth than the partial understanding needed for a concatenative system. > > An applicative system only makes sense as a declarative system. A > > concatenative system also makes sense as an imperative one. > I'm not sure what precisely you mean by "declarative" and > "imperative", I've never heard anyone express misunderstanding of that particular distinction before; it's one of the most common and deep-running distinctions in computer languages. Could you clarify? > but this may be an apt description. An applicative system like > the one I've described can be used to represent all kinds of > things, including numbers, functions, and procedures. A Joy-style > system can only be used to represent procedures, but procedures > that push expressions representing, say, numbers onto the stack > can be used to emulate numbers. This is what I meant by the distinction between imperative and declarative. What you seem to be saying is that concatenative notations cannot be used in declarative languages. This is false. The entire purpose of the formalization developed in the Joy documentation is to discuss how Forth-like languages can be formalized; the result is the concept of 'concatenative' languages, wherein each word is actually a function which takes a stack as a parameter. > > I think this last feature is what attracts me to > > concatenative logic. > > Modern computers are by their nature imperative, and many > > of the problems > > solvable by them are well-described by imperative processes > > ("programs"). > I think you have again touched on one of Joy-style systems' best > advantages: they are more straightforwardly executed on > modern machines. I do not doubt this; but, I think programs > expressed in purely-applicative languages may be easier to > reason about (reasons for this will be given at the end of > the post). With a nice metasystem, however, I suspect that programs > expressed with a purely-applicative language could be executed > just as efficiently as Joy-style programs. Likewise, with > a bit of overhead, I suspect that one could reason well about > Joy-style programs. The reason I found Joy so facinating is that it's so easy to reason about its programs. There's no special overhead; on the contrary, it's _easier_ than any other system I've seen. Now, at the time I hadn't seen a purely applicative system, and even now I don't completely understand them, so it may be that Joy has a competitor. I seriously doubt it, though; I liked J and APL, but they don't even come close to Joy, and they are tested and complete languages. > So, the question of which system is superior is a matter of taste... This may be true. I find it very believable. > > > B(BB)B = C(BB(BBB))B > > > The first one states the associativity of composition; > > Ah. In that case, would it be true that: > > concat concat == stack:(abc--cab) concat swap concat; > > expresses the same thought in a concatenative form? > Yes... this states the associativity of "concat". This is similar to > composition, I suppose. No, it's identical to composition. Don't forget that this is a _concat_enative language. Concatenation is composition. > > I find that to be easier to read; is that just me? > Well... You have cheated by using "stack:(abc--cab)". > (although I see your note below now that Joy has a primitive for it). I've used a little of my own language -- but in that case you've cheated as well by using nothing but your own language. ;-) Yes, Joy has a pre-named combinator for that -- after all, it's a trivial stack manipulation involving only 3 items. I don't think it's cheating; it's just using a variant of the notation. > The combinatory axiom could be made more readable if one > used combinators other than "B" and "C" to express it; > for instance, > foldl 2 B = foldr 2 B > expresses the same thought, where "foldl" and "foldr" are a certain > series of combinators, such that "foldl 2" and "foldr 2" have these > properties: > foldl 2 f x y z = f(fxy)z > foldr 2 f x y z = fx(fyz) Sure, I could do the same thing (use different combinators tailor-made for the problem) and get an equally simple result. The point is that it's not a good idea to use a concatenative axiom in a proof-battle against a concatenative language ;-). Try using an applicative axiom and watch me fumble (I suspect). [concat] 2 foldl == [concat] 2 foldr. >From this and the previous statement about concat we can derive that 2 foldl == dup dip i; 2 foldr == dup [dip] cons dip i; The RHS of those definitions sounds like a 50s acappella hit -- "do wop dip oooooh". Anyhow, from this I could derive the definition for 'foldr' and 'foldl' themselves, except that I don't feel that I have the knowledge for it. I guess I should try anyhow: The most obvious translation is this: foldl == [[dup dip i] dup dip] swap times pop; foldr == [[dup [dip] cons dip i] dup dip] swap times pop; but I'm sure there's a better way of writing that which doesn't use the text of the previous definitions. > > > for, from it it follows that > > > B(BB)B F G H = C(BB(BBB))B F G H > > > BB BF G H = BB(BBB)F B G H > > > B(BFG)H = B(BBBF) B G H > > > B(BFG)H = BBBF BG H > > > B(BFG)H = B BF BG H > > > B(BFG)H = BF(BGH) > > Sorry, I'm lost. I don't know most of the combinators, > > certainly not by > > their ISO standard one-letter names. > Hmm... perhaps my spacing was confusing... anyway, the only > combinators > I used were "B" and "C": Ah! I see. I got confused by the F, G, and Hes. I didn't realize you were using named variables. > "C" takes a function's converse. Converse? This isn't the usual use of the word, is it? I thought a converse was a logical negation. Okay, my mistake; in that case the above example is written swap swap == id or to imitate your language's actions more precisely (by requiring a function as a parameter), C == [swap] dip; C C i == i; rather than not not == id > > > This was mentioned above... I don't see that those five > > > "syntactic elements" are essential in every applicative language > > > or how they are evident in the one I've presented... > > They were certainly present in the language you _had_ > > presented. Four of > > them are still present in the language you've created for this post; > > Concatenative languages still only need three. > Let's see... here were the four you listed: > > 1) Currying is explicit. > > 3) Name definition is not present (but you cover it later, > > and you don't > > deny that it's needed); note that in the examples you give, > > the functions > > are simply thrown away, no possibility of reuse. > > 4) Curry order modification is carried out by the absence of @ (as I > > mentioned, you've only made grouping implicit). > > 5) Parameter counts are needed to actually run/use any of > > your functions; > This does not really make sense to me. These seem to be just > observations about my system, rather than "syntactic elements". Of course they're observations about your system. You explicitly asked me to state how the elements I had listed previously were present in your system. If you want the list stated in the form of general rules, take the four-item list I gave elsewhere, or take the five-item list and consider #2 and #4 the same thing. > In particular, how can #3, "Name definition is _not_ present" > (emphasis added) be a syntactic element? I was pointing out that in the four lines of examples you gave, there was no use of name definition. I was claiming further that your examples were not representative of a faintly useful language. > Anyway, what I said (and which you have not contradicted) was that > combinators "are not particularly special elements in the language", I _have_ contradicted it; I've even provided arguments against it. > meaning that that the language does not have complicated constructs > for referring to them. I don't think the complexity of the construct is a measure of the "specialness" of the element. The question is simply whether the element behaves fundamentally differently from other elements in your language, thus justifying putting it in a seperate class. > However, even though the language does > not have special constructs for dealing with them, they might > be treated quite specially by the execution system; in this > sense, as you have said, they are not so "ordinary". How can the language _avoid_ having special constructs for combinators? I don't have a clue how this can be done. You have to *at least* rewrite all combinators down to the basis set (the combinators from which everything else is constructed in your theory), and then you have to have special parsing support for each of the basis combinators. > > It seems to me that the compiler will have to be specially > > written in order > > to merely use many of the combinators. > Exactly. Of course, the Joy interpreter also needed special support > for its combinators "swap", "dup", "dip", and such. No it doesn't -- they behave exactly identically to all other Joy words. They take a single stack as input and produce a single stack as output. The compiler doesn't have to even have a clue what they do. > As you've commented, > the only support it needed to give them is the ability to execute > them (as atomic programs). In other words, they are the same thing as all the other words in the system. Pun unintentional. Joy has a few classes of special constructs: - definitions and scoping (syntax) - quotation (syntax) - words - functional words (swap, +, etc.) - semi-functional words (put, get, etc.) ...and others (we've talked about that before, I'm not trying to rerun that discussion). The compiler only has to know about the three main distinctions; a proof system has to know about four distinctions (as shown), possibly more in order to treat each semi-functional word appropriately. > My system might require more elaborate > support for the analagous combinators; but, then again, perhaps > it won't. Let's suppose that your system uses functions and three combinators: K, D, and C (I don't expect that these are the ones actually needed, I don't remember that). Your system will require: - atomic functions and types (assuming that you don't go "whole horse" and just define everything in terms of the basic combinators, which is certainly possible but VERY expensive) - user-defined functions, types, and combinators - K - D - C (and the usual "other things" we so much enjoy discussing. ;-) Note that I listed each of the combinators seperately; this is because they are the basis combinators, the fundamental expressions of meaning. They can't be expressed in any simpler way; they have to be directly understood. The rewriting rules for them _have_ to use explicit variables, and the parser has to know all of those rewriting rules. Oh, I'm not listing your procedural subsystem, because I have no idea how you're going to handle that. > > Here's two reasons to prefer a concatenative system (this > > adds to your reasons): > > 3) Translation to machine code is simpler than translation of an > > applicative system. This is minor; Tunes doesn't want to > This is a good point, and may be a true advantage of > Joy-style systems. > But, I suspect that there may be clever ways to compile procedures > written as purely-applicative expressions. I'll grant that just because Joy's simplicity is obvious doesn't mean that it's the only possible way. I'd like to hear some of your ideas. You discuss some in an addendum to this post, and I've split that into a seperate reply (since this is too long anyhow). > > practical experience > > seems to indicate that people writing metaprograms are > > comfortable using concatenative languages. > Well... I don't really know much about Postscript, but I don't > see how purely-applicative systems would have particular difficulty > with metaprogramming. Of course, I don't know of any practical > purely-applicative systems with flexible metaprogramming, > since I don't know of any practical purely-applicative systems > at all. But we _do_ know of a few impractical purely-applicative systems, and lots of practical (and impractical) applicative languages, and a couple of combinative languages. Of our entire collection, the one with the most extensive metaprogramming use is Postscript, a concatenative language, even though there are so many more languages of the other types, many of which explicitly attempt to be useful for metaprogramming. Most of the other languages seldom even see metaprogramming; Lisp and its relative use it for minor tasks, but almost never for total program generation. > > But don't let my skepticism slow you -- one problem I have is an > > unwillingness to work on something which isn't new to me. > > Innovation is my > > goal, but it's not the only good goal -- you're working on > > refinement, and that's also a noble goal. > I guess it could be called "refinement", although there aren't really > any particular systems that I'm starting with to refine. Anyway, Sure you are -- applicative systems. The single most commonly use programming paradigm; so common that most people aren't even aware of it. > this kind of system does seem like an interesting thing to > work on, whether it is new or not... Right. > Let's see... I mentioned that I'd give some reasons why programs > written as purely-applicative expressions might be easier to > reason about than similar ones written in a Joy-style system, > so now I'll give a couple: I'll clip most of this, although its really good stuff -- this email has gotten too long. See the Joy website for examples of reasoning in Joy; it's _very_ effective, quite brief, and entirely legible. > 1) In a purely-applicative system of the kind I've described, > it is always possible to replace a sub-expression with > one of equivalent meaning, while this is not the case > in the Joy system, because of Joy's heavy dependence on > a quotation construct. To illustrate, suppose in Joy one > had a program Not true -- as you admit below, the quotation construct has little to do with it. The problem is caused by being able to treat quotations as lists. If that were not possible, or if it were restricted in certain ways, there would be no problem even in this circumstance. However, I'm not interested in handling that restriction; being able to automate reasoning about programs in such a simple way is a powerful feature. > Such a complication does not occur in an analagous > purely-applicative system. The procedure It would cause a "problem" if the "analogous" applicative system were capable of modifying its own programs. > foo (plus 0) > could be freely rewritten as > foo I > regardless of the structure of "foo", if the system had > realized that the expression "plus 0" means the same > thing as "I". And the exactly analogous expression "0 plus foo" could be freely rewritten as "foo" in Joy. Don't confuse parentheses with brackets. Note that there wasn't even a need for an 'id' placeholder. > This problem with Joy was mentioned in the manual > (in the section "A rewriting system for Joy"); they presented > some possible remedies, but none which they really liked. Yes. > Anyhow, this is not a fatal problem of Joy, only > a slight impediment to reasoning about programs (since Entirely correct. It _does_ cause an impediment; the interesting thing is that it itself (treating programs as lists) is a form of automated reasoning about programs. This suggests that the problem it causes _might_ be fundamental, one of the classic Godel incompleteness problems (as phrased for concatenative languages, of course). In other words, reasoning about programs may be easy; but reasoning about programs which are themselves rasoning about programs is not easy. I don't think it's that simple, though; there are a lot of list operations which can be done which don't affect the rewriting rules -- for example, concat and cons. The main problem comes when taking apart lists (cdr), or doing other operations which can cause boundary conditions. > 2) Many fundamental equivalences of Joy programs that one > might like to show require restrictions, while > similar equivalences of purely-applicative expressions > can be taken unrestrained. Specifically, one might > like to think upon seeing a Joy program of the form This is _really_ heavily unfair. Those things aren't "fundamental equivalences"; they're rewriting rules. Rewriting rules of that nature are a thing used by syntax-based languages, and you honestly can't expect them to apply to a non-syntax based one. The fact that some of them do anyhow is almost amazing. The actual rewriting rules Joy has are very simple; they state things like: if "t == a b c" and "d e a b c f g == z y x" then "d t f g == z y x". In other words, direct text replacement is reasonable. > The analagous equivalence in a purely-applicative system > is that > @@@C f x y > can be rewritten as > @@f y x > This can be accepted without restraint for all expressions > "f", "x", and "y" of the system. That's because combinators in your system have syntactic effect. In order to do that, I'd have to define: C == dip [swap] i; [y] [x] [f] C == [x] [y] f Note that this imitates the heavily syntactic approach of your system by using the only syntactic feature of Joy (quotation). This makes Joy have all of the drawbacks of your system while still keeping all of its own drawbacks -- probably not a good idea ;-). > But, overall, the purely applicative approach still seems to me > to be the best way to found a system. But, if you can see > any awful problems with the approach, I'd still like to > hear, of course. The nastiest one I've seen is the lack of ability to *do* things in the language. You have to add another language on top. I also suspect that even the strength of your functional language, functions, will be very hard to read and maintain without automated help. APL functions don't use many combinators, but APL programmers recommend deleting the program and rewriting it based on the comments rather than trying to understand the program. Forth programmers don't do that. > (at this point, I'm going to begin rambling on some > thoughts about *implementing* the system) This looks like a good point at which to split this message. I'll reply later, hopefully in a helpful rather than argumentative tone. -Billy From iepos@tunes.org Tue, 15 Feb 2000 17:26:48 -0800 (PST) Date: Tue, 15 Feb 2000 17:26:48 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Joy, Lambdas, Combinators, Procedures As you say, this has turned into quite a fun discussion... I'm going to do a bit of clipping in my reply... > > a system might like to formalize as primitive expressions > > the number literals 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... > > strictly, this is not allowed in a purely applicative system since > > it would require an infinite number of primitives. Instead, one > > could take as primitives only the numbers 0 through 9 (and maybe > > "ten"), and then refer to the other numbers using multiplication and > > addition. > > Sorry, but as much as I like the theory you're bringing to the > table, I really, really hate messing up a good language with theory. I was a bit unclear on this: it is not my intent to apply such strict restrictions to external programming languages of the system. The purpose of that comment was to clarify my definition of "purely applicative" which I've been using a lot throughout this discussion. A practical system will probably use languages that are not entirely purely applicative in this strict sense. > Don't forget that the purpose of a programming language is to allow > people to express things to the computer. Another thing I was not clear about: when I've said that I wanted my system to be based on a purely applicative language, I meant that _internally_ it would use a (nearly) purely applicative language. When I've been talking about languages of the system, I think maybe you've misunderstood me to be talking about external languages that the user uses (there may have been some cases where this is what I meant though)... But, incidentally, if I was a user wanting to use my system, I would probably wouldn't mind the external language being nearly purely applicative, also. > Having a finite number of "primitives" is > not required by any theory you've stated; it's simply an arbitrary > restriction you've placed upon yourself. This statement reveals a misunderstanding by what I mean by "primitives". By a "primitive" of a language, I mean an expression of the language which has a precise meaning that is not determined by the meaning of its parts. If a language has a scheme of expressions with similar meaning, then those expressions are not primitives (by the definition of what I mean by "primitive"). With this definition of "primitive" in mind, the idea of a language having an infinite number of primitives is quite bizarre. In general, it would be impossible to understand sentences in such a language, since the language would essentially have an infinitely large vocabulary, which you could probably never grasp. When I mean "primitive" in this sense, I'll try to say "word" from now on, since I may use "primitive" in other ways sometimes. > > Well, your "stack(--)" construct would then be much like a > > lambda construct of applicative systems (note that it requires the > > use of variables like "a", "b", and "c"). This would probably cause > > complications in proof systems. > > Not at all, because the pseudovariables would not intermingle at all with > functionality. It's nothing more than a new naming convention for a subset > of the combinators, really. The 'C' combinator under this system has the > name 'ba' or 'stack:(ab--ba)'. (In my original design and implementation > under Pygmy Forth I accepted input of the simple form 'abc--cba' rather than > requiring the 'stack:()' notation.) I can see that you like to think of "ab--ba" as a word, just another way of writing "swap", rather than as a complex expression formed from "--". If you only used this abbreviation for a few stack-operators (i.e., if you renamed "dup" to "a--aa", "pop" to "a--", and a few others), then it really would make little difference to the system. On other hand (and this is the original point I was trying to get across), if in general you accepted all expressions of the form "...--..." in the system, then this would be quite a change to the system. The system could no longer be implemented in the "switch-case" way that I suspect it is, since there would be an infinite number of primitives of the form "...--...". In other words, whereas now Joy has specific support for just a few stack-operators like "swap" and "dup", if this new "--" notation were adopted in general, then it would not be enough for Joy to support just a few of the expressions of the form "...--..."; it would have to support them all. This would likely be done in one of these two ways: 1) by adding special support in the interpreter loop to handle execution of expressions of the form "...--...". 2) by preprocessing the program, replacing expressions of the form "...--..." by equivalent expressions formed from just "dip", "swap", "dup", and "pop" (and possibly a few others). I'm guessing you'd prefer the second approach (a textual preprocess may not really be necessary; the replacement could happen as part of the parse, if the system parses its text before executing it; or, it could do the replacement at the last second, when interpreting the expression); either way would require significant modification to the system. But still, it would not necessarily be bad. I'm still quite interested in knowing of an "abstraction algorithm" that could be used to rewrite "...--..." expressions using just "dip", "swap", and such. You've hinted that "dip" will be important, although surely other operators will also be needed. Abstraction algorithms are quite interesting things to me... There are probably several different algorithms that might be used... For instance, "abc--cba" might be written either as swap [swap] dip swap or as [swap] dip swap [swap] dip Incidentally, that these two are the same corresponds to a fairly fundamental Combinatory Axiom that C.BC.C = BC.C.BC or in other words that BC(B(BC)C) = B(BC)(BC(BC)) > The reason I'm calling your grouping into question is complexity; I believe > that your system has very important categories which you're ignoring. > For example, your primitive functions bear little resemblance in behavior to > your primitive combinators. Your functions simply use their parameters; > your combinators have to select and choose parameters. I'm not sure what you mean by "primitive functions" (combinators are a kind of function); perhaps you are talking about my procedure primitives like "get" and "put" (i.e., analogues of "scanf" and "printf"). These primitives are of a different sort than combinators, and probably would be grouped together when presenting the system (actually, they are not so different from combinators as I think you've perceived; you've misunderstood how I plan to go about doing procedures; in a separate post, I'll try again to explain how I plan on doing them). > > The precise meaning of "combinator" may vary a bit among uses, but > > calling all functions "combinators" is getting a bit nonstandard. > > This is practically true for applicative languages, but breaks down almost > completely for concatenative ones. In such languages, ALL words (including > so-called combinators and special forms) are actually functions from a stack > to a stack. This is true (except for informal primitives like "put" and "get", which can't really be modelled as just functions from stacks to stacks); but, usually by a "combinator" in Joy, I am just referring to things like "swap", "dup", "pop", and maybe "dip" and "concat" and some others, but not "+", "-", "1", "2", and such. This use of "combinator" with respect to Joy is a little vague; but, in case it is not clear, in an applicative system, by "combinator", I mean a function that takes some parameters (through currying), and results in some application of those parameters to each other (i.e., a "combination" of the parameters). A few examples of combinators: - "f\f f": a function that takes a parameter "f" and results in "f f", the application of "f" to itself. (This is sometimes called the mockingbird combinator). - "x\y\x": a function that takes two parameters "x" and "y" (through currying) and results in "x". In other words, this combinator takes a parameter of "x", and results in a function that always returns "x". This is sometimes called the K combinator. - "x\y\z\ xz(yyy)(zx)". This is a weird combinator with no common name and seemingly little significance. There are also of course lots of other interesting combinators like B, C, W, and S... > In addition, according to the foundational axioms of > K-complexity theory, all functions can be built from a small set of specific > combinators (I seem to recall S and K) combinators, It is true that the S and K combinators can be used alone to form all other combinators, and even a bit larger class functions which I call "pure functions", but to say that S and K alone can be used to form _all_ functions is getting a bit silly. For instance, S and K cannot be used to form the function of "fatherhood", which given a person, yields his father. They also cannot be used to form numeric functions like "+", "-", "*", and "/" in the ordinary sense (although they can be used to form similar functions that operate on Church Numerals or representations of numbers). > so all functions can therefore be regarded as combinators in any > system (including yours). Not so. But, this is true in a certain sense... there are interesting ways in which pure functions can be used to emulate numbers, propositions, and lists. It is conceivable that pure functions could be used to represent even other things like people, pictures, sounds, and programs, but this would be a bit weird, and I don't plan to do it in my system. > > But anyhow, that definitions and system dictionary were "not > > special" was not my main point; my main point (which you have > > not contradicted) was that a special language construct is not > > needed for definitions. > > I have contradicted that; although it's true that no special syntax is > needed (an obvious point proven many times over) It is obvious that you have misunderstood what I meant by "special language construct". By this, I meant a special syntax, like Joy's "==". Surely you would not contradict me and then immediately acknowledge that what I said was true. > The fact that all of your combinators can be expressed in terms of a > few simple combinators doesn't change the fact that all combinators -- > especially including those few ones -- require special parsing from > the compiler. I have admitted several times that I don't know of any techniques for efficiently compiling programs expressed in the style I've suggested (although I have hope that efficient techinques do exist and that I will discover them at some point). I do know of some inefficient techniques of interpreting them, anyway. There's one thing somewhat relevant to this that I may not have made clear yet... whereas many programming languages only provide ways of representing programs, languages of the type I've described will cleanly be able to represent other concepts, such as numbers, propositions, lists, functions, as well as programs. I think some enlightenment may result from comparing my language(s) to human languages like English, rather than to programming langauges. Of course, languages in my system will probably not really have the expressiveness of English (at least, not initially), nor will they have the vagueness or syntactic complexity of English. But, as it is possible in English to refer to, say, the number "two", so will it be possible to refer to the number two in a language in my system. A purely applicative language could conceivably be used for purposes other than programming; it could be used for human communication (this would be unusual), or it could be used as a device for a formal logic system, without involving automatic computers. > > > Speaking of immediate execution, how would your system > > > handle Forth-like > > > metaprogramming? Does the code have full access to all of > > > the unparsed > > > source? > > > Probably yes... As an example, if a user has been working on writing > > a complicated program, he would probably have given a name to > > a field for the program's text. Conceivably, this name could > > occur within the text itself, so that the program could refer > > to its own text cleanly. > > This is not Forth-like; in fact, it's a superset of Forth's capabilities > which will force you to re-parse the file every time it's used. That's > pretty brutal. I do not see how it requires me to re-parse the file every time it is used. Perhaps you mean "every time it changes"; I don't know... > interpreter hasn't touched yet. I don't believe that this is even possible > with your system; all text has to be parsed before anything can be > evaluated. I don't have a system. Neither do I have my mind set on any particular technique for interpreting text that user's type in. Anyway, it doesn't really take very long to parse text, so this doesn't really seem to matter too much to me (well, perhaps there might one time be a huge specification file; I suppose if a user changed one bit in such a huge file, it might be worth it if the system can find a way to reparse just a part of it without reparsing the whole thing; this is something I have not ruled out). > Your system doesn't currently have any > obvious ways to reach inside a completely applied function and modify it, > but you can play with the variables about to be passed to a function. (In > this respect, Joy is clearly _much_ more expressive, since manipulating a > quoted program as a list allows arbitrary modification.) This is an interesting comment. I've thought using adding a "reification" primitive (as Fare calls it) which is a function that given a thing, yields a data-structure that represents that thing; this would allow "arbitrary manipulation". I'm not sure that I would really need to use this though. (by the way, since there may be several ways of expressing things in a language, one might make such a "reification" primitive as yielding a set of expressions, rather than a particular one; if one did this, one would probably also need a "pick" procedure that one could use to get the system to arbitrarily pick a member of a set; but... I need to clarify on how procedures will be dealt with) > > On the other side of metaprogramming, languages in the system will > > probably provide ways of referring to the systems own > > low-level structures, including, for instance, procedures for > > peeking and poking at the system's raw memory, or doing raw > > I/O. Using these procedures, the system could change the > > way it works (it could even change itself so that it did not > > work at all). > > Until the theory is worked out, it can be nice to have the > raw capability, even if we know that someday it'll go away. I always want to have raw capability. I want full power over my system. But, of course it might be nice if the system asked for confirmation (involving authentication to make sure it is really me and not the imposter who stole my keyboard) before doing something it thought might be dangerous. But this would be quite nice feature that I don't expect to implement anytime soon; it would naturally be the job of the shell more than anything else. > I have to agree with you that definitions don't _have_ to be included in > either of our systems, though. It would be a bear to work with, but it > would be *possible*. Yeah, as an example, one could look at Unlambda. It doesn't have definitions, and it is a bear to work with, as you say, probably due to the fact that it doesn't allow definitions. The next issue brought up, how my system will deal with procedures, I'll reply to in a separate post... By the way, if anybody else reads these posts and has something interesting to say, feel free to chime in. - "iepos" (Brent Kerby) From iepos@tunes.org Tue, 15 Feb 2000 17:30:02 -0800 (PST) Date: Tue, 15 Feb 2000 17:30:02 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Procedures in CLIP (was: Joy, Lambdas, ...) I'm going to go back over how procedures might work in my system ("CLIP") since I wasn't too clear before... This will describe how the system internally models procedures, as well as how programmers would probably refer to them. > > By "procedure", I mean a specific command > > that something should be done, or the idea of that specific thing > > getting done. In this sense, here are some examples of some procedures > > (written in English): > > > > - Go home. > > - Reboot yourself. > > - Output "Hello World" to the user. > > "Square a number." Okay, I'm with you. Wait... I don't think you are. Each of the above three examples expresses an action ("going home", "rebooting oneself", "outputting 'hello world'"). But, what action does "Square a number" express? What would the system do if you told it, "Square a number". Suppose we supply a specific number; for instance, does "Square the number 3" then express an action? This may be getting close to representing a real action, the action of visually changing the number "3" into the number "9" inside one's mind. But, this is quite a useless action that I don't really care if my system can perform. > So 'procedure' connotes a process taking place in time, I guess this is correct... But, procedures like "go home" and "reboot yourself" should be distinguished from events (as I'll call them) like - the event of Joe going home (on January 5, 2000 at 5:00PM) - the event of Robo rebooting himself (at such-and-such time) A "procedure", as I use the word, is about the same as a "command" or an "instruction". One procedure could be executed several different times by several different systems. > whereas a function could denote a step of that > process, but doesn't state anything about time. No... A function, as I use the word, is just an abstract mapping from keys to values... not a step of a process... > So to make a function act procedurally, you need to transform it into a > "completion" by attaching a next-thing-to-do to it, correct? No... I'm not sure what you mean by "make a function act procedurally". There is really nothing procedure-like about, say, the function of squaring; I'm not sure how you would make it act like a procedure. > > And, in this sense, the idea of "Output" (like the "printf" of "C") > > is not itself a procedure, but a function that takes a string > > and then yields a procedure. > > Okay, so it's possible to have computed procedures. This only makes sense. This is correct. Another way of saying it is this: one can refer to a procedure using an expression other than a single word. > Let me try to write a brief program: > > & > ! @exec @Output @stringify @@+ 4 5 > stop This is not correct. "&", "!", and "exec" are all strange things that you do not need. Also, "stop" is not needed in this example. If I understand what you want the program to do, it should be written: @Output @stringify @@+ 4 5 This is a procedure that outputs the string "9". There is no need to wrap it with "exec" or "&". When you type in an expression representing a procedure, the interpreter will probably respond by executing the procedure, and then giving you another prompt. Now, suppose you wanted the system to do the procedure twice, then you would compose the two procedures together using "B", as in: @@B @Output @stringify @@+ 4 5 @Output @stringify @@+ 4 5 (in a moment, I will clarify on why composition is used, and what I meant by "procedure completions" and when "stop" might be used). Since we are composing the same thing with itself we could use "WB" (the self-composition combinator; in general, "W" takes a binary function and yields a similar unary one with a duplicated parameter, so to speak): @@WB @Output @stringify @@+ 4 5 This would be a procedure that outputs the string "9" two times. > Okay, that makes a little more sense. (I'm not allowing myself to think > about the logic involved in a function which executes a procedure -- that > doesn't even begin to make sense -- but it's the first thing I thought of.) I agree... a "function which executes a procedure" doesn't even begin to make sense. Now, to explain "procedure completions" and why "B" can be used to concatenate procedures... Often, actions that the system might execute have a certain time of completion. For instance, if the user typed in @Output "hello" into a shell, after a while the system would be finished executing it, and it would be time for the shell to give another prompt (well, depending on the nature of the shell, this is probably what it would do). By the way, the technique of procedure completions that I'm about to describe is not the only way that one could do procedures in an applicative system of the style I'm considering, but it is one that I find quite natural... Using it, it is not necessary to have extra primitives for concatenating procedures or returning results from procedures. Basically, a "procedure completion" is a string of things to do, although it is distinct from a large procedure or a list of procedures. It may seem odd that we make up a new notion "procedure completion", when seemingly we could emulate it using just a large procedure or list of procedures. But, doing it this way will be fruitful, I think, so bear with this explanation... Using procedure completions, it is possible to think of a procedure "p" as a function that takes a procedure completion "x" (the thing to do after the procedure terminates) as a parameter, and yields another procedure completion, the procedure completion of doing "p" and then going on to do "x". Stated again (this is important), a procedure is a function that takes a procedure completion and yields a procedure completion. For instance, the procedure @Output "hello" can be thought of as a function that takes a procedure completion and yields a procedure completion. Essentially, the procedure completion that results is the same as the one that is taken except that the action of outputting "hello" is prepended. For instance, @@Output "hello" other-things-to-do is a procedure completion to output "hello" and then go on to do "other-things-to-do". Remember that a shell in the system will probably not actually want to be given a procedure completion, but instead a procedure (a shell could possibly be constructed which took a procedure completion rather than a procedure, but I would prefer that it take a procedure). But still, it is useful to talk about procedure completions (there are also some times when one may explicitly want to refer to a procedure completion in the system; this will be described later). Now, since "@@Output 'hello' other-things-to-do" is a procedure completion that outputs "hello" and then does those other things, it follows that @@Output "hello" @@Output "world" other-things-to-do would be a procedure completion that outputs "hello" and then outputs "world". What I'm leading up to is that @@B @Output "hello" @Output "world" is a procedure that outputs "hello" and then outputs "world" because, by the definition of "B", applying this procedure to a procedure completion "z" results in @@Output "hello" @@Output "world" z As a reminder, the rewrite rule of "B" could be expressed @@@B x y z == @x @y z Hopefully this is starting to make sense now. Be sure to keep in mind the distinction between procedures and procedure completions... Now, procedures that "take parameters" (I would be reluctant to call them procedures at all) are emulated using functions onto procedures. For instance, "Output" is a function that takes a string and yields a procedure (this has been mentioned before). Procedures that yield results are emulated in a similar way. Recall that most procedures (that don't yield results) take as a parameter just a completion, a thing to do after the procedure is done executing; in contrast, a procedure with a result (for instance, an "Input", which reads a line from the keyboard, and results in a string) takes not a procedure completion, but a function onto a procedure completion. As an example, a procedure that takes an input using "Input" and then spits the resulting string back out with "Output" could be written: @@B Input @C Output That might not have made sense... Maybe it will make sense if you note that when this procedure is applied to a completion "z", you get (by definition of "B"): @Input @@C Output z Remember that "Input" is a function that takes as a parameter another function (a function that, given the resulting string of Input, tells what procedure completion to do next). Here, we have applied "Input" to the function "@@C Output z". Essentially, this function "@@C Output z" is a function that takes the resulting string "s" and yields "@@Output s z", a procedure completion that outputs the string "s" and then goes on to do "z". I'm guessing this may still seem a bit foggy, as it is not entirely familiar even to me, who has made up this system. The best thing to do now, then, seems to be to give a few more examples... For a bit, I'll stay away from procedures that give results, since these are probably the most confusing. One simple example... @@B Rinse-it @@B Scrub-it Dry-it is a procedure that Rinses, Scrubs, and then Dries it (where "Rinse-it", "Scrub-it", and "Dry-it" are simple procedures). This is just a reminder that composition can be used to concatenate simple procedures. Using syntactic sugar "." for composition, this might be written as: Rinse-it . Scrub-it . Dry-it This procedure, when applied to a completion "z", gives Rinse-it (Scrub-it (Dry-it z)) This is a procedure completion that does Rinse-it, Scrub-it, and Dry-it, and then goes on to do "z". It could have been written in that "@" syntax as @Rinse-it @Scrub-it @Dry-it z Now for another example... Now suppose we have more general primitives "Rinse", "Scrub", and "Dry", which are functions (taking a piece of dishware or something) that yield procedures; these procedures would be kind of like the "Output" mentioned earlier, which is also a function that yields a procedure. Now, with these three primitives, we want to form a function that takes a piece of dishware and yields a procedure that does all three things to it; using a lambda, we could express this procedure as x\ (Rinse x . Scrub x . Dry x) or alternatively, using "@", x\ @@B @Rinse x @@B @Scrub x @Dry x This procedure could be expressed without a lambda, using the "N" combinator, as N B Rinse (N B Scrub Dry) or alternatively, @@@N B Rinse @@@N B Scrub Dry What exactly the "N" combinator is I won't go into (unless you ask), except that it can be constructed from other combinators as "B(BS)B" and that its rewrite rule is: Nfghx = f(gx)(hx) The "N B" combinator (or just "NB") turns out to be quite interesting; it is associative in that "NB (NB x y) z" is the same in all cases as "NB x (NB y z)". This "NB" combinator turns out to act as an addition function among the Church Numerals, and so we might write that last procedure with syntactic sugar "+" for "NB" as Rinse + Scrub + Dry This is quite a clean result (I wouldn't expect it to always be quite this pretty). Remember that this whole thing is a function that takes a piece of dishware "d" and yields the procedure Rinse d . Scrub d . Dry d This procedure is a function that takes a procedure completion "z" and yields Rinse d (Scrub d (Dry d z)) or in other words, @@Rinse d @@Scrub d @@Dry d z For curiosity, a program analagous to "Rinse + Scrub + Dry" I think would be written in Joy as dup dup Rinse Scrub Dry Anyway, do you understand now the way of doing procedures that I've described? (If you do, you might try to give me another example program)... It seems to be quite a nice, pure approach that, as far as I know, has never been attempted (of course, it may have been attempted and I not know). Now, I'll restate again how procedures that yield results work... Whereas "Rinse-it" is a function that takes simply a procedure completion (telling what to do next, after "it" is done being rinsed), a result-yielding procedure takes not a procedure completion, but a function onto a procedure completion. For instance, we'll take "Pick-favorite-dish" as a simple result-yielding procedure. We can think of it as a function that takes a function (a function that tells what to do next, when applied to the favorite dish). For example, suppose the system wants to execute the procedure completion Pick-favorite-dish foo The system would execute this by deciding what its favorite dish was (call it "d"), and then going on to execute the procedure completion "foo d". Procedures that yield multiple results might be emulated as a single-result-yielding ones that yield a list; but, more likely one would use a procedure that took a multiple-parametered function (multiple-parametered through currying). For instance, let "Pick-two-things" be some procedure that yields two results in this way. Then, when the system executes the completion Pick-two-things foo It will do the action associated with "Pick-two-things", and then do "foo x y", or in other words, "@@foo x y", where "x" and "y" are the two results that the procedure yielded. One other interesting thing... Y (Output "hello") is a procedure completion that outputs "hello" over and over again (forever), if "Y" is the recursion combinator. For, by the rewrite rule of "Y", it is the same as Output "hello" (Y (Output "hello")) That was a procedure completion that did "Output 'hello'" over and over again. If one wanted a procedure instead, one could use K (Y (Output "hello")) This procedure ignores its parameter, the "next thing to do". By the way, the above discussion may have made it seem that all procedures in my system will be functions that take a procedure completion (or else a function onto one) as a parameter, telling what to do after the procedure terminates. This may not exactly be the case; some procedures, for example, might take two procedure completions as parameters, the first telling what to do next if the procedure "succeeds", and the other telling what to do if it has some sort of failure. I'm not really sure in general how I'll handle failures with procedures in my system; there are probably several different ways to go about it... The system will probably need lots of primitives involving procedures, like "Output", "Input" and such (actually, the initial system will probably use lower-level primitives for writing to the screen, and will construct its own "Output" and "Input"). As I've remarked before, several primitives involving concurrency and fields will probably be needed... One random thought... I mentioned earlier that I'd give an example use of "stop", which is what I'll do now (well, I'll get to it in a second)... Sometime in the system I'll probably use a "meanwhile, start working on this" primitive, a procedure primitive (actually a function onto a procedure, like "Output") which initiates another task. Suppose we call this primitive "start", then here would be an example program involving it: start (Output "hello" stop) . Output "world" This would be a program that outputs "hello" and "world" but in an order undetermined. The "start (Output 'hello' stop)" part is a procedure that executes very quickly probably; it tells the system to start working on "Output 'hello' stop", but the system only has to _start_ working on it before proceeding; it might not be _finished_ doing "Output 'hello' stop" before going on to do "Output 'world'". If an interpreter executes this program, it might even give the next prompt before outputting "hello", although "world" will definitely be outputed. Note that "start", as I've used it, takes a procedure completion, like "Output 'hello' stop" as a parameter, but not a procedure, like "Output 'hello'". One could conceivably construct a "start" that takes a procedure rather than a completion (in fact, such a thing could be constructed from the original "start" as "B start stop"), but the one that takes a completion is better to me. Anyway, they are both interdefinable (the ordinary "start" can be defined from the other start as "B otherstart K"). Hopefully this explanation was helpful... There are still many issues relating to the system that I haven't decided on. One of the biggest questions, and this was mentioned in the last post, is "How will the system be implemented?". Actually, I've already started implementing a system. I have something that works but that is not usable. Within a few weeks it should be ready for toy purposes (for instance, outputting the first 100 prime numbers, or a number guessing game, or finding a solution to the Towers of Hanoi). Currently, numbers are implemented using "int"s. This does not really satisfy me; I plan on going back and reimplement the system to use Church Numerals, but this will probably take time and I do not want to wait any longer to get a system that can at least do something interesting. One other question about the system that I haven't really decided on is: "Will there be a logic system integrated into the system? If so, how would such a system work (internally and externally)?"... Such a logic system would probably store Theorems in some way, sometimes deriving new ones and tossing out old ones; such a system would probably assist in normalizing expressions... For instance, if a user had told the system that "foo" is an integer and that "foo*2 + 1 = 9", then the logic system might help the normalize "foo" to "4", if "foo" needed to be normalized at some point. Such a logic system would be a very nice thing to have in a system, but there are a few pitfalls I suppose... Logic systems tend to be of a very fragile nature (I guess almost all computing systems are of a fragile nature, but logic systems are the worst, I think). For instance, suppose a user told the system that "K = CK" (or "0 = 1" or something of this sort)... This could tend to result in a system crash, since if the system really accepted it, then by some simple reasoning it could replace any expression for any other expression; the system might decide to recompile itself to be more "efficient" (but accidentally incorrect, although provably correct, since anything at that point would be easily provable by the system). One solution to this "fragileness" problem is for the system to be careful when applying newly obtained information in certain ways; it may wish to restrict itself only to highly trusted axioms when verifying the correctness of a new compilation of itself, for instance. Also, the system might do common-sense consistency checks before accepting new information at all, to make sure it is not absurd; but, a full consistency check (meaning a proof that the added information will not cause everything to be provable) in general would not be possible, I think, according to "Godel's theorem", unless the system is already blown (in which case everything would be provable, including that the system is consistent). Another problem with having a logic system is that logic problems are indefinitely difficult. In general, not even a brute-force algorithm exists for testing theorems, much less an efficient one (at least, this is true if the system contains the so-called "first order predicate logic" which is undecidable). If programs were distributed, one would probably not want to include logic problems in the program; rather one would "compile" in an ordinary procedure to find a solution (this would require some indefinitely difficult work on the original machine, but the program could then be straightforwardly executed on the other machines)... but then, the program might want to be provided in its original form too, in case a target system is smart enough to find a better solution. Anyway, that was quite a vague thought (sometime I'll have to think of some specific problems to illustrate what I'm saying)... I don't think I'm going to build in a logic system into the prototype system, although I might try to make one on top of it at some point, storing the theorems using fields. A nice logic system is something that I think I'm going to need before a nice efficient compiler (with provable correctness) can be implemented; also, a logic system is a valuable tool by itself, from a user's perspective. And ... the rest of your comments I'll reply to in another post... - "iepos" (Brent Kerby) From btanksley@hifn.com Fri, 18 Feb 2000 18:01:17 -0800 Date: Fri, 18 Feb 2000 18:01:17 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Joy, Lambdas, Combinators, Procedures > From: iepos@tunes.org [mailto:iepos@tunes.org] > I'm going to do a bit of clipping in my reply... Cool. This makes the message a reasonable length! > > Having a finite number of "primitives" is > > not required by any theory you've stated; it's simply an arbitrary > > restriction you've placed upon yourself. > This statement reveals a misunderstanding by what I mean by > "primitives". > By a "primitive" of a language, I mean an expression of the language > which has a precise meaning that is not determined by the meaning > of its parts. If a language has a scheme of expressions with > similar meaning, then those expressions are not primitives > (by the definition of what I mean by "primitive"). That's a facinating definition. Yes, it makes sense. > When I mean "primitive" in this sense, I'll try to say "word" from > now on, since I may use "primitive" in other ways sometimes. Okay. I'll try to observe that as well. What's kind of odd is that your first definition of your purely applicative language included the phrase "a finite number of primitives," and I've been trying to figure out what you meant ever since. Evidently it was merely a typo, since in your definition it's impossible to have an infinite number of primitives. > > > Well, your "stack(--)" construct would then be much like a > > > lambda construct of applicative systems (note that it requires the > > > use of variables like "a", "b", and "c"). This would > > > probably cause complications in proof systems. > > Not at all, because the pseudovariables would not > > intermingle at all with > > functionality. It's nothing more than a new naming > > convention for a subset > > of the combinators, really. The 'C' combinator under this > > system has the > > name 'ba' or 'stack:(ab--ba)'. (In my original design and > > implementation > > under Pygmy Forth I accepted input of the simple form > > 'abc--cba' rather than > > requiring the 'stack:()' notation.) > I can see that you like to think of "ab--ba" as a word, just another > way of writing "swap", rather than as a complex expression formed > from "--". Right. Although I also recognise that it IS a complex expression, of course. The point is that it's perfectly conceptually correct to picture it as a primitive word, in which case the system can have an infinite number of primitives. > If you only used this abbreviation for a few stack-operators > (i.e., if you renamed "dup" to "a--aa", "pop" to "a--", and a few > others), then it really would make little difference to the system. Also correct. Keep in mind that I'm not interested at this point in things which "make little difference". The interesting thing about this addition is that it makes no difference to the number of programs which can be written or to their provability, but rather makes it easier to write them without making it harder to read. > On other hand (and this is the original point I was trying to get > across), if in general you accepted all expressions of the > form "...--..." > in the system, then this would be quite a change to the system. > The system could no longer be implemented in the "switch-case" > way that I suspect it is, since there would be an infinite number > of primitives of the form "...--...". Almost correct. The system already can't be implemented as a switch-case; its implementation is like Forth's: while next_word(source): if ( dictionary lookup ) then ( execute ) else if ( interpret as number ) then ( push on stack ) else error; The new addition I'm pondering is changing this from a sequence of if statements to a list of tests and actions, like this: def system.lex(source): while not source.eof(): internalSource = source.copy() for parser in parser_list: lexeme = parser.lex(internalSource) if lexeme: return lexeme There will be extensive in-language support for parsing and lexing, of course, to make it easier to define complex parser_list additions. One reason I want this is that I'd like to be able to imitate the way Rebol handles some of its things. For example, %filename.txt is actually a reference to the named file; http://www.rebol.com is the named site, and can be read from via the usual 'read' functions. It's _really_ cool how they do it, and I think I can do way better. > In other words, whereas now Joy has specific support for just a few > stack-operators like "swap" and "dup", if this new "--" notation were > adopted in general, then it would not be enough for Joy to support > just a few of the expressions of the form "...--..."; it would have > to support them all. This would likely be done in one of > these two ways: Actually, it would have to support a finite-length subset of them all. I'm not a stickler for impossible things. The very fact that I'm only specifying lower-case alphabetic letters on the left hand side limits the complexity to 26 inputs, and I'm not interested in more (although I don't see a reason to limit it to fewer, even though I strongly hope nobody ever uses that many). It's possible to have as many outputs as you want, but it'll consume a huge amount of stack space for no obvious purpose (I doubt I'll artificially limit it, though; if someone wants to write "26DUP" they're free to do so). > 1) by adding special support in the interpreter loop to handle > execution of expressions of the form "...--...". > 2) by preprocessing the program, replacing expressions of the > form "...--..." by equivalent expressions formed from > just "dip", "swap", "dup", and "pop" (and possibly a few > others). Both of these are wrong, as I mentioned above. The main point I mentioned the stack:() notation was to explain another, more interesting idea: that the action to take when the usual lexing fails doesn't need to be printing an error message. > I'm guessing you'd prefer the second approach (a textual preprocess > may not really be necessary; the replacement could happen as > part of the parse, if the system parses its text before > executing it; or, it could do the replacement at the last > second, when interpreting the expression); either way would > require significant modification to the system. But still, > it would not necessarily be bad. That's a good way of looking at it, so long as you don't take the word "preprocess" too literally. It's almost literally postprocessing, actually; it only tries to interpret a word as a stack:() notation if it doesn't make sense any other way. > I'm still quite interested in knowing of an "abstraction algorithm" > that could be used to rewrite "...--..." expressions using > just "dip", "swap", and such. You've hinted that "dip" will > be important, although surely other operators will also be needed. I don't have the algorithm written out, although I'm sure I can find it if I need to; I've seen it before. I was actually planning on working it out by hand, since it's not immensely complicated as such algorithms go :-). If you'd like to see it, I'll try to schedule some time to work it out, and if I can't, I'll look it up again. > I'm not sure what you mean by "primitive functions" (combinators are > a kind of function); The problem is the use of 'primitive'. Let's skip this point for now; it's become too confused to be discussed. > > > The precise meaning of "combinator" may vary a bit among uses, but > > > calling all functions "combinators" is getting a bit nonstandard. > > This is practically true for applicative languages, but > > breaks down almost > > completely for concatenative ones. In such languages, ALL > > words (including > > so-called combinators and special forms) are actually > > functions from a stack to a stack. > This is true (except for informal primitives like "put" and "get", > which can't really be modelled as just functions from stacks > to stacks); Actually, they _can_ be modelled that way. The problem is that some other things may be affected, and the accuracy of the modelling is less than it would be with other words. But in all cases the inaccuracies are clearly delimited, affect only that one word, and can be easily dealt with theoretically. This is as opposed to words like @Input in your language, which cannot be dealt with in the same way as functions returning values. > but, usually by a "combinator" in Joy, I am just referring to > things like "swap", "dup", "pop", and maybe "dip" and "concat" > and some others, but not "+", "-", "1", "2", and such. > This use of "combinator" with respect to Joy is a little vague; > but, in case it is not clear, in an applicative system, by > "combinator", I mean a function that takes some parameters > (through currying), and results in some application > of those parameters to each other (i.e., a "combination" of the > parameters). A few examples of combinators: In that case, 'dip' is the only combinator in the above list, since it's the only one which actually results in any application. I think another good definition of 'combinator' is: a function which can take other functions as parameters. Thus + isn't a combinator, but swap or drop can be; dip always is. This definition works with your language as well, although I'm happy to agree that it doesn't need to. > - "f\f f": a function that takes a parameter "f" and > results in "f f", > the application of "f" to itself. (This is sometimes called > the mockingbird combinator). > - "x\y\x": a function that takes two parameters "x" and "y" (through > currying) and results in "x". In other words, this combinator > takes a parameter of "x", and results in a function that > always returns "x". This is sometimes called the K combinator. > - "x\y\z\ xz(yyy)(zx)". This is a weird combinator with no common > name and seemingly little significance. Cool. So you also have a compact notation for your combinators (which you could add to a variant of your language in the same sense that I've considered adding the stack:() notation to mine). > > In addition, according to the foundational axioms of > > K-complexity theory, all functions can be built from a > > small set of specific > > combinators (I seem to recall S and K) combinators, > It is true that the S and K combinators can be used alone to form all > other combinators, and even a bit larger class functions which I call > "pure functions", but to say that S and K alone can be used > to form _all_ functions is getting a bit silly. For instance, > S and K cannot be used to form the function of "fatherhood", > which given a person, yields his father. They also cannot be > used to form numeric functions like "+", "-", "*", and "/" > in the ordinary sense (although they can be used to form similar > functions that operate on Church Numerals or representations of > numbers). Of course they only operate on representations of numbers. Doing anything else is impossible. They also only operate on representations of people and paternity -- Darth Vader could not use a function to capture Luke, nor vice versa (even if Luke had been willing to admit that Vader was his father). I don't understand your point. > > so all functions can therefore be regarded as combinators in any > > system (including yours). > Not so. But, this is true in a certain sense... there are interesting > ways in which pure functions can be used to emulate numbers, > propositions, and lists. It is conceivable that pure > functions could be used to > represent even other things like people, pictures, sounds, > and programs, > but this would be a bit weird, and I don't plan to do it in my > system. Given the theory behind your system, I don't see how you have any choice. You *are* using a finite number of things to represent all possible concepts, surely an infinite set. You _have_ to permit your user to use the finite number of elements to represent whatever they want. > > > But anyhow, that definitions and system dictionary were "not > > > special" was not my main point; my main point (which you have > > > not contradicted) was that a special language construct is not > > > needed for definitions. > > I have contradicted that; although it's true that no > > special syntax is > > needed (an obvious point proven many times over) > It is obvious that you have misunderstood what I meant by > "special language construct". By this, I meant a special > syntax, like Joy's "==". Surely you would not contradict me and > then immediately acknowledge that what I said was true. I think you've forgotten what we were discussing here -- I was claiming that the 'define' primitive formed a special category of primitives. > > The fact that all of your combinators can be expressed in terms of a > > few simple combinators doesn't change the fact that all > > combinators -- > > especially including those few ones -- require special parsing from > > the compiler. > I have admitted several times that I don't know of any techniques > for efficiently compiling programs expressed in the style I've > suggested (although I have hope that efficient techinques do > exist and that I will discover them at some point). I do know > of some inefficient techniques of interpreting them, anyway. The question isn't whether your programs can be compiled into some representation and then used. I'm confident that we'll work that out. My statement was much simpler: each of your fundamental words will require special _parsing_. Each of those words will change the interpretation of the words which follow them. Thus, each of them has to be treated specially by the parser, and the parser has to be written for them. It's like Forth's distinction between words and numbers; numbers are handled by the compiler, and they're a completely seperate case from words. > There's one thing somewhat relevant to this that I may not have made > clear yet... whereas many programming languages only provide > ways of representing programs, languages of the type I've described > will cleanly be able to represent other concepts, such as numbers, > propositions, lists, functions, as well as programs. I think > some enlightenment may result from comparing my language(s) to > human languages like English, rather than to programming langauges. You're 'accusing' other languages of faults which are your own constructs. Your language is not the first 'declarative' programming language ever developed; in fact, it's not even an especially good one, since its syntax is so focussed on your style of maps. You've also invented a concept of "true representation", in which your language is gifted with the ability to truly represent things, while all other languages falsely represent them. Since you've never defined your concept of representation, it's impossible to guess what this means to anyone. > > > > Speaking of immediate execution, how would your system > > > > handle Forth-like > > > > metaprogramming? Does the code have full access to all of > > > > the unparsed > > > > source? > > > Probably yes... As an example, if a user has been working > > > on writing > > > a complicated program, he would probably have given a name to > > > a field for the program's text. Conceivably, this name could > > > occur within the text itself, so that the program could refer > > > to its own text cleanly. > > This is not Forth-like; in fact, it's a superset of Forth's > > capabilities > > which will force you to re-parse the file every time it's > > used. That's > > pretty brutal. > I do not see how it requires me to re-parse the file every time > it is used. Perhaps you mean "every time it changes"; I don't > know... Pronoun confusion. Let me rephrase: "...which will force you to re-parse the file every time it [the metaprogramming capabilities] are used." Sorry. > > interpreter hasn't touched yet. I don't believe that this > > is even possible > > with your system; all text has to be parsed before anything can be > > evaluated. > I don't have a system. Yes, you do. We're talking about it. > Neither do I have my mind set on any particular > technique for interpreting text that user's type in. Anyway, it > doesn't really take very long to parse text, so this doesn't > really seem to matter too much to me It's certainly much, much more than the 1*O(n) operation my language requires. Justification: each word in the input stream has to be lexed, then each word has to be reduced to its internal representation (which is likely a matter of a dictionary lookup and replacement with the appropriate number of "S"es, "K"s, and other fundamentals). Then you have to load the words into their tree structure, reading left to right and performing pinhole optimization. You use a stack to keep track of unresolved applications, since the most recent application "@" you reached is always the appropriate place to attach the next parameter (even if the parameter is itself an application). Finally, you can optimize (a task which I'll assume takes zero time), and then evaluate the result, using at least depth first traversal (assuming that you only want to execute it as a program). The biggest time-sink there is the depth-first traversal; it's n*log(n). That's not as bad as I'd feared. Of course, adding the possibility of source modification we get a new worst case of n^2 log(n). But that's silly (hmm, it's not the silliest worst case -- it would be possible to not terminate; THAT is silly ;-). > (well, perhaps there might one > time be a huge specification file; I suppose if a user changed > one bit in such a huge file, it might be worth it if the system > can find a way to reparse just a part of it without reparsing the > whole thing; this is something I have not ruled out). The problem with your metaprogramming suggestion is twofold: - it requires knowledge of the source file's name and total overall structure, as well as the willingness to muck about with the original source code. - it requires a complete re-parse and re-compilation after every change, since it's possible to change any part of the program, including a part which has already been executed. You can see that metaprogramming is NOT encouraged under these circumstances. Note the difference from how Forth does it: - Forth only allows metaprogramming in the part which hasn't been parsed yet. This means that re-reading is never needed. - Forth allows modification relative to the current location in the source file. - Forth's modifications have zero effect on the source itself; they don't even affect the internal representation of the source. Technically, all they do is discard source and/or provide alternate source. > > Your system doesn't currently have any > > obvious ways to reach inside a completely applied function > > and modify it, > > but you can play with the variables about to be passed to a > > function. (In > > this respect, Joy is clearly _much_ more expressive, since > > manipulating a > > quoted program as a list allows arbitrary modification.) > This is an interesting comment. I've thought using adding a > "reification" primitive (as Fare calls it) which is a function > that given a thing, yields a data-structure that represents > that thing; > this would allow "arbitrary manipulation". I'm not sure that I would > really need to use this though. (by the way, since there may > be several > ways of expressing things in a language, one might make such a > "reification" primitive as yielding a set of expressions, rather > than a particular one; if one did this, one would probably also need > a "pick" procedure that one could use to get the system to > arbitrarily pick a member of a set; but... I need to clarify > on how procedures will be dealt with) That's a very interesting concept. Yes, that would give your language as much power as Joy has now, which is likely more than Forth has. The problem is that it adds a new set of structures to your language which were not present before and are not present in Joy. Forth uses metaprogramming to make up for its lack of function modification, but a Forth programmer doesn't have to learn a new language to use it, since the language being modified is the same one he's coding in. The language you're having your programmers modify looks entirely different from the one they're coding in. > - "iepos" (Brent Kerby) -Billy From btanksley@hifn.com Tue, 22 Feb 2000 13:45:32 -0800 Date: Tue, 22 Feb 2000 13:45:32 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Procedures in CLIP (was: Joy, Lambdas, ...) > From: iepos@tunes.org [mailto:iepos@tunes.org] > I'm going to go back over how procedures might work in my system > ("CLIP") since I wasn't too clear before... This will describe how > the system internally models procedures, as well as how > programmers would probably refer to them. Good. > > > By "procedure", I mean a specific command > > > that something should be done, or the idea of that specific thing > > > getting done. In this sense, here are some examples of > > > some procedures (written in English): > > > - Go home. > > > - Reboot yourself. > > > - Output "Hello World" to the user. > > "Square a number." Okay, I'm with you. > Wait... I don't think you are. Each of the above three examples > expresses an action ("going home", "rebooting oneself", > "outputting 'hello world'"). But, what action does "Square a number" > express? What would the system do if you told it, "Square a number". Square a number, hopefully. Possibly by multiplying it by itself, although I didn't specify that. What would you expect it to do? Hmm, if you really wish to be literal, I would expect it to take a representation of a number as input, and produce a representation of the square of that number as output. I'm not sure what blocks you from understanding the need for that, but perhaps it'll help if I use a more complicated example: "Compute the DES encryption of a 8-byte quantity." The two procedures express the same basic idea. > Suppose we supply a specific number; for instance, does > "Square the number 3" then express an action? This may be getting > close to representing a real action, the action of visually changing > the number "3" into the number "9" inside one's mind. But, this is > quite a useless action that I don't really care if my system > can perform. Certainly you care. Now, I'm not claiming that your _user's_ machine will ever perform that action, but it's certain that somebody's machine will: either your machine when it compiles the procedure, or the user's machine when it runs the procedure. > > So 'procedure' connotes a process taking place in time, > I guess this is correct... But, procedures like "go home" and > "reboot yourself" should be distinguished from events (as I'll > call them) like > - the event of Joe going home (on January 5, 2000 at 5:00PM) > - the event of Robo rebooting himself (at such-and-such time) > A "procedure", as I use the word, is about the same as a "command" > or an "instruction". One procedure could be executed several > different times by several different systems. Right. Perhaps I should have said "a process taking place _through_ time" rather than "in time". A procedure consists of zero or more steps which have a partial order to them with respect to time. > > whereas a function could denote a step of that > > process, but doesn't state anything about time. > No... A function, as I use the word, is just an abstract mapping from > keys to values... not a step of a process... "Mapping" is a form of a verb, "to map". In other words, a mapping may be used as part of a procedure to map one thing to another. A mapping is a noun; a procedure is (essentially) a verb. A procedure uses mappings (functions) to map values to other values. If this is impossible, then why on earth did you include both functions and procedures in one language? > > So to make a function act procedurally, you need to > > transform it into a > > "completion" by attaching a next-thing-to-do to it, correct? > No... I'm not sure what you mean by "make a function act > procedurally". > There is really nothing procedure-like about, say, the function > of squaring; I'm not sure how you would make it act like a procedure. By making it take place as part of the procedure (as opposed to simply denoting its existance). You can't make it act like a procedure, of course; but you can make it an atomic part of a procedure, and by doing so you make it act "in time"; you make it capable of becoming an event. > > Let me try to write a brief program: > > & > > ! @exec @Output @stringify @@+ 4 5 > > stop > This is not correct. "&", "!", and "exec" are all strange things that > you do not need. Also, "stop" is not needed in this example. If I > understand what you want the program to do, it should be written: > @Output @stringify @@+ 4 5 > This is a procedure that outputs the string "9". There is no need > to wrap it with "exec" or "&". When you type in an expression > representing a procedure, the interpreter will probably respond > by executing the procedure, and then giving you another prompt. In other words, the interpreter implicitly prepends the "&" operator when appropriate. There will be times when you will need to prepend it yourself. (I don't care what the operator looks like, I'm just using a handy key.) This is different from what would happen if the user typed "@@+ 4 5"; in that case, I would expect the interpreter to cleverly realize that the user probably wanted the system to perform the map thus indicated (even though it's not a procedure and thus has no meaning in terms of being performed). > Now, suppose you wanted the system to do the procedure twice, > then you would compose the two procedures together using "B", as in: > @@B @Output @stringify @@+ 4 5 @Output @stringify @@+ 4 5 > (in a moment, I will clarify on why composition is used, > and what I meant by "procedure completions" and when "stop" might > be used). Since we are composing the same thing with itself we > could use "WB" (the self-composition combinator; in general, > "W" takes a binary function and yields a similar unary one > with a duplicated parameter, so to speak): W == [dup] dip i; > @@WB @Output @stringify @@+ 4 5 > This would be a procedure that outputs the string "9" two times. > > Okay, that makes a little more sense. (I'm not allowing > > myself to think > > about the logic involved in a function which executes a > > procedure -- that > > doesn't even begin to make sense -- but it's the first > > thing I thought of.) > I agree... a "function which executes a procedure" doesn't even > begin to make sense. Grin. The problem is that I'm trying to think of your system as being consistent and simple, when it's actually not. It's got a very complex interpreter, which tries its best to "do what you mean". The thing is, this actually makes sense for your language, since your language isn't about _doing_ things; it's natural that your interpreter has to be smart in order to _do_ things using it. > Now, to explain "procedure completions" and why "B" can be used > to concatenate procedures... > Basically, a "procedure completion" is a string of things to do, > although it is distinct from a large procedure or a list of > procedures. It may seem odd that we make up a new notion > "procedure completion", when seemingly we could emulate it > using just a large procedure or list of procedures. But, > doing it this way will be fruitful, I think, so bear with > this explanation... What is a "large procedure"? And isn't a procedure completion actually the same thing as a "list of procedures"? > Using procedure completions, it is possible to think of a > procedure "p" > as a function that takes a procedure completion "x" (the thing to do > after the procedure terminates) as a parameter, and yields another > procedure completion, the procedure completion of doing "p" and then > going on to do "x". Right. In fact, it's essential to think of it this way, since otherwise it doesn't work in your system. > @@Output "hello" other-things-to-do > is a procedure completion to output "hello" and then go on to > do "other-things-to-do". Remember that a shell in the system > will probably not actually want to be given a procedure completion, > but instead a procedure (a shell could possibly be constructed > which took a procedure completion rather than a procedure, > but I would prefer that it take a procedure). But still, it > is useful to talk about procedure completions (there are also > some times when one may explicitly want to refer to a procedure > completion in the system; this will be described later). Isn't a procedure completion just a special type of procedure? (Or is it the other way around?) > Be sure to keep in mind the distinction between procedures > and procedure completions... Urf. I'm certain I don't understand, because everything you've said makes sense unless I try to assume that there's a distinction between procedures and procedure completions. > Now, procedures that "take parameters" (I would be reluctant to > call them procedures at all) Really. Why? > are emulated using functions onto > procedures. For instance, "Output" is a function that takes a > string and yields a procedure (this has been mentioned before). Really. I was wondering about that. Why not just make them procedures? > Procedures that yield results are emulated in a similar way. > Recall that most procedures (that don't yield results) take as > a parameter just a completion, a thing to do after the > procedure is done executing; in contrast, a procedure with > a result (for instance, an "Input", which reads a line from > the keyboard, and results in a string) takes not a procedure > completion, but a function onto a procedure completion. Urk. Now I'm getting confused. Okay, so procedures with inputs are emulated using functions. This makes sense, since functions take inputs. Cool. But procedures with _outputs_... Um... Gee, that's _NASTY_. Okay, wait! I see why you're doing this: procedures aren't functions, so they don't 'return' values in the same manner as a function does. Okay. Sorry for the visible thinking process. I think I understand the reason for this; I'm missing one little thing, though, so I'm going to try repeating what you said. "@Input someFunction" means to get some input, apply someFunction to it (resulting in a procedure completion), and then execute the resulting procedure completion, right? So in a sense @Input is a REALLY special function, more like a combinator, right? I don't know. This whole thing is just mind-blowing. Okay, so at this point it's pretty clear that "someFunction" has to be _executed_ at runtime; unlike the other functions we've been using, it's _forced_ to act like a procedure. I don't like this: I think that your model of input and output needs to be refined. It just doesn't seem to work with the rest of your ideas. Perhaps one of the other applicative languages might provide a useful theory? Oh, and one more question. Suppose I wanted to input a number and print the number which is five more than it. Let @Input handle numbers only (so that we don't have to worry about errors). @Input @Output @+ 5 Is that right? We want to take two functions and one parameter, and produce a single function which takes one parameter (which will be passed to the +). If Joy were subject to the same restrictions, the program would look like this: [5 + Output] Input. Do I get it? Another possible example would be [5 swap / Output] Input. Or @Input @Output @@C / 5 > As an example, a procedure that takes an input using "Input" and > then spits the resulting string back out with "Output" could > be written: > @@B Input @C Output > That might not have made sense... Maybe it will make sense if > you note that when this procedure is applied to a completion "z", > you get (by definition of "B"): > @Input @@C Output z Hmm. Yes, I think I see this. And then if we write the result of Input as "Input", we get: @Output "Input" z Thanks to the 'swap' action of @@C. Right? > I'm guessing this may still seem a bit foggy, as it is not > entirely familiar even to me, who has made up this system. Good call. > Now for another example... Now suppose we have more general > primitives "Rinse", "Scrub", and "Dry", which are functions > (taking a piece of dishware or something) that yield procedures; > these procedures would be kind of like the "Output" mentioned > earlier, which is also a function that yields a procedure. > Now, with these three primitives, we want to form a function > that takes a piece of dishware and yields a procedure that does > all three things to it; using a lambda, we could express this > procedure as Note that lambda actually does a poor job of modelling this action; the problem is that lambda notation implies that the object being named remains constant within a given lexical scope, and an object which is at one moment 'wet' and at the next 'dry' is not constant. > x\ (Rinse x . Scrub x . Dry x) x\Dry Scrub Rinse x > or alternatively, using "@", > x\ @@B @Rinse x @@B @Scrub x @Dry x Um... Let me try... Hmm. I can't figure out how to have a function take a parameter which is returned by another function, and then return something which is used by yet another function. Okay, after a weekend of relaxation it makes a little more sense. It's merely a matter of convention -- we have to choose which order the parameters will go in. I choose to put the parameter list before the return function. @@Rinse dish Dry Hmm, it seems to me that multiple returns would be easy to perform (or at least would be no harder than single returns). Your opinion? Finally, it's clear to me that you'd need a combinator to handle anything more than two levels of that. What combinator would that be? How would you Wash, Dry, then Rinse a dish using this design? > or in other words, > > @@Rinse d @@Scrub d @@Dry d z > > For curiosity, a program analagous to "Rinse + Scrub + Dry" > I think would be written in Joy as > dup dup Rinse Scrub Dry Ideally, it would be written (using my design) as Rinse Scrub Dry. However, using your design, your writing works; I would more commonly write it as dup Rinse dup Scrub Dry; since we only duplicate before the use. Makes sense to me. Note that in my design, 'Dry' returns the dried plate, so the whole function returns a clean and dry plate. Your function doesn't return anything. My function can be modified to act like yours by rewriting it, intuitively enough, as follows: Rinse Scrub Dry drop. Anyone who's every washed dishes and dropped at least one will see the effects of this procedure/function. > Anyway, do you understand now the way of doing procedures that > I've described? (If you do, you might try to give me another > example program)... How did I do? > It seems to be quite a nice, pure approach > that, as far as I know, has never been attempted (of course, it > may have been attempted and I not know). I don't really like it very much, honestly. It may be pure, but I fail to see any benefits this purity brings. Joy is equally pure, but is _much_ easier to work with procedurally, mainly because Joy is theoretically based on composition, and procedure execution has the same behavior as function composition. Whoops! I was just checking my Python URLs for the week, and I read the explanation of Continuations at http://www.ps.uni-sb.de/~duchier/python/continuations.html; guess what? It turns out that your procedures match the formal version of the theory of continuations. Cool. I've used continuations before, but I haven't seen them explained that way. Take a look. > Procedures that yield multiple results might be emulated as > a single-result-yielding ones that yield a list; but, more > likely one would use a procedure that took a multiple-parametered > function (multiple-parametered through currying). For instance, > let "Pick-two-things" be some procedure that yields two results > in this way. Then, when the system executes the completion Ah, yes. This is much easier than my concept of having a list of single-input functions. > Note that "start", as I've used it, takes a procedure completion, > like "Output 'hello' stop" as a parameter, but not a procedure, > like "Output 'hello'". One could conceivably construct a > "start" that takes a procedure rather than a completion > (in fact, such a thing could be constructed from the original > "start" as "B start stop"), but the one that takes a completion > is better to me. Anyway, they are both interdefinable > (the ordinary "start" can be defined from the other start > as "B otherstart K"). Start is a non-function. That is, its only purpose is to do something, not to 'return' something. Right? > of itself, for instance. Also, the system might do common-sense > consistency checks before accepting new information at all, to make > sure it is not absurd; but, a full consistency check (meaning a > proof that the added information will not cause everything to > be provable) in general would not be possible, I think, according to > "Godel's theorem", unless the system is already blown > (in which case everything would be provable, including > that the system is consistent). Well, you _can_ check that the new theorem doesn't contradict the axioms, can't you? > Another problem with having a logic system is that logic > problems are indefinitely difficult. In general, > not even a brute-force algorithm exists for testing > theorems, much less an efficient one (at least, this is true if > the system contains the so-called "first order predicate logic" > which is undecidable). Even simple Boolean logic is NP-hard. > - "iepos" (Brent Kerby) -Billy From iepos@tunes.org Wed, 23 Feb 2000 15:52:41 -0800 (PST) Date: Wed, 23 Feb 2000 15:52:41 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Joy, Lambdas, Combinators, Procedures This is the third part of my reply to that message you sent a while back... I have your two more recent messages and will reply to them too, sometime... this message is still way too long, so i'll skip through some things (I read them of course, but just do not have time to reply to them), but I'll at least try to address your direct questions. > > I still do not quite get exactly what you mean by "parameter counts". > > A function can't be 'executed' in any way I know of until all of its > parameters are filled (otherwise its value is undefined). Thus you have to > keep count of parameters. This is more complicated than it might seem; you > can't simply trace through the code and keep a type stack, as you can with > Joy; you have to parse the entire source and consider a potentially huge > tree of parameters. > > For example, in your language, is this an error? > > @@@ackermann 4 5 6 > > (Ackermann is a two-parameter function.) That would be an expression that would probably have no denotation under the language (although it is "well-formed", in a certain sense). Any expression involving this expression would also have no denotation. I guess you could say that it is an error. If you fed this expression (or an expression involving this expression) to an interpreter, it would probably complain that the expression does not denote a procedure (the interpreter can only execute expressions that denote procedures). > > Not sure what you mean by "select" combinator or "runtime" > > combinators. > > A 'select' combinator is an example of a runtime combinator. Think of the > Forth word 'PICK', which takes an integer "n" and pushes onto the stack the > n-th item. Joy has no such combinator, although it would be easy enough to > build one by using a loop and 'dip'. The point is that sometimes you don't > know which item to take until runtime. Ahh... this is interesting. If I understand what you mean by "select", then in my system an analogous "select" would have these properties (these are just examples): select 2 f x y z == f z x y z select 1 f x y z == f y x y z select 0 f x y z == f x x y z The last two properties could be written more simply as: select 1 f x y == f y x y select 0 f x == f x x Essentially, applying a "select n" to a function of so-many parameters yield a similar function but with a copy of its "nth" parameter inserted at the front. It might be wondered whether such a "select" could be constructed in my system... it is certain at least that each specific "select n" can be constructed (since they are all combinators). For instance, "select 0" is simply "W" (this is analogous to a "0 select" of Joy being the same as just "dup"). Also, it turns out that "select 1" is the same as "BW . C" or in other words "B(BW)C". Now, the question of whether a fully general "select" (i.e., a function that takes a natural number and yields the corresponding "select" combinator) could be constructed is a bit more difficult... the answer depends on what primitives are available for number-manipulation. After a bit of study, I see that in general, "select n" can be constructed as B (iterate n B W) (foldl n C) This depends on two other series, "iterate" and "foldl", as I've called them. These two series would probably be easy enough to construct in any decent number system. They have the generating rules: iterate 0 == KI iterate (n+1) == SB (iterate n) foldl 0 == KI foldl (n+1) == (S(CB) . BB) (foldl n) I suspect that there might be a simpler way of generating "foldl"... Anyway, then, using a "primrec" one could directly form these series as iterate == primrec (K(SB)) (KI) foldl == primrec (K(S(CB) . BB)) (KI) By the way, the series "n\ foldl n C" (which was used in forming the series for "select" in my definition) is itself interesting; it is quite similar to select, except that it _moves_ the parameter (so to speak) instead of copying it. The way in which I've formed "select" works essentially by duplicating the parameter and then moving the copy (so to speak). > > I don't > > see how it would be such a headache to add a new combinator in > > my type of system... all combinators can be constructed from just > > a few (for instance, "B", "C", "W", and "K"), so if the system could > > handle procedures written using those few combinators, then it could > > handle procedures written using other ones, by just replacing > > occurences of the new ones by their definitions in terms of > > the old ones. > > Look up K-complexity on the web -- it's the theory of reducing functions to > just a few combinators. (Your compiler could theoretically work that way.) > The problem is that it's a very hard task; Hmm... I'm not familiar with "K-complexity", but this is incorrect, I think. It is really quite an easy task to rewrite any combinator (expressed using lambdas) as a combination of, say, the "I", "B", "C", "S", and "K" combinators. There is a straightforward algorithm for doing it; I'm not sure what its order is, but in practice (at least for relatively simple combinators that people might want to deal with), it takes practically no time at all to carry it out... I won't take the time here to detail how exactly the algorithm works (this is done on my web site, though, under the section, "Lambda Construct and Completeness"), but I will just give an example of how the combinator f\g\h\x\ f(gx(hx)) can be written with "I", "B", "C", and "S", using the algorithm... f\g\h\x\ f(gx(hx)) f\g\h\ Bf(Sgh) f\g\ B(Bf)(Sg) f\ B(B(Bf))S C(BB(BBB))S You can verify that this combinator, "C(BB(BBB))S" is the same as the original by checking to see that when applied to the four parameters to get the right answer: C(BB(BBB))Sfghx BB(BBB)fSghx by C's rewrite rule B(BBBf)Sghx by B's rewrite rule B(B(Bf))Sghx by B's rewrite rule B(Bf)(Sg)hx by B's rewrite rule Bf(Sgh)x by B's rewrite rule f(Sghx) by B's rewrite rule f(gx(hx)) by S's rewrite rule Anyway, this just shows that it is a fairly easy mechanical task to rewrite lambda-expressions using the combinators "I", "B", "C", and "S" (and occasionally "K", when dealing with functions that ignore some of their parameters). > > > An applicative system only makes sense as a declarative system. A > > > concatenative system also makes sense as an imperative one. > > > I'm not sure what precisely you mean by "declarative" and > > "imperative", > > I've never heard anyone express misunderstanding of that particular > distinction before; it's one of the most common and deep-running > distinctions in computer languages. Could you clarify? Well, I guess usually "imperative" is used to describe systems in which the "command" or "procedure" play a fundamental role, whereas "declarative" is used to describe systems where the "statement" (expression of information) plays a fundamental role. If this is what you meant, then I guess I would say that my system is either both "imperative" and "declarative" or neither one; procedures will play an important role in the system, and statements will also (when I begin working on the logic part of the system), but neither the "procedure" or "statement" has extra-special status in the system. Actually, it has been a thought in my head that the distinction between "statements" and "commands" is somewhat artificial... Commands can be re-expressed as statements, and vice versa. For instance, the English command, "Go get me something to drink." could be re-expressed as a statement (with little or no change in meaning) as "I want you to get me something to drink." Similarly, the statement, "Two plus two equals four." could be expressed as a command as "Note that two plus two equals four." > > However, even though the language does > > not have special constructs for dealing with them [combinators], they might > > be treated quite specially by the execution system; in this > > sense, as you have said, they are not so "ordinary". > > How can the language _avoid_ having special constructs for combinators? I > don't have a clue how this can be done. You have to *at least* rewrite all > combinators down to the basis set (the combinators from which everything > else is constructed in your theory), and then you have to have special > parsing support for each of the basis combinators. I have indeed gotten away without having special parsing support for combinators (although this doesn't seem like an amazing feat to me). Here is an excerpt from the parser of my system: expr *toexpr(){ if(str[siz]=='@'){ expr *x,*y; siz++; x=toexpr(); y=toexpr(); if(x==NULL || y==NULL) return NULL; else return (expr *)App(x,y); }else return name_toexpr(toprim()); } Basically, it checks to see if the string starts with a '@'; if so, it skips over it and then descends into recursion; otherwise, if it doesn't start with '@', then the expression must be a word, in which case "toprim()" is used to parse it: char *toprim(){ char *s=NULL; int i=0; int max=0; for(;;){ char c=str[siz]; if(i>=max){ s=mem_reget(max,max*2+1,s); max=max*2+1; } if(c=='.'){ s[i]=0; siz++; return s; }else if(c==0 || c==EOF){ return NULL; }else{ s[i]=c; i++; siz++; } } } [sudden realization that I could have just used strchr()]... This is a messy way of reading the word; words are terminated by "." in the system. Anyway, my point is that the parser does not specially mention any combinators; combinators are expressed using just ordinary words. Of course, certain combinators are mentioned specially in other parts of the system. By the way... here is an example expression that the parser recognizes: @@def."Y".@@B.M.@@C.B.M. The string literal "Y" is treated as if it was a word by the parser, although it is handled a bit specially by "name_toexpr" (which usually does a dictionary-lookup). If I were to rewrite the system, I would probably make this kind of syntax sugar be handled by the parser instead, but it doesn't really matter I guess. By the way, there is an actual system... it can be found at http://www.tunes.org/~iepos/clip/clip-0.3.6.tar.gz It is kind of weird and doesn't do very much right now. When it starts, it runs the list of procedures in the "boot" file (each line is parsed and executed before the next is parsed; this is so that if one line defines a new word, then the next lines can make use of the new word). Currently, all that "boot" does print a little column of "o"s down the screen (using recursion), and installs a hook so that when you press a key, the representation of the key appears on a certain place on the screen. There are several things about the system that I'm not really satisfied with (aside from the fact that it is incomplete and slow)... but I won't go into that now... > Let's suppose that your system uses functions and three combinators: K, D, > and C (I don't expect that these are the ones actually needed, I don't > remember that). Your system will require: > > - atomic functions and types (assuming that you don't go "whole horse" and > just define everything in terms of the basic combinators, which is certainly > possible but VERY expensive) types... types are interesting things that I've been thinking about lately... my current system is rather untyped. The main ability that one would associate with "typed systems", I suppose, is the ability to arbitrarily construct restricted functions, i.e. functions that only meaningful when applied to a certain class of parameters. For instance, in my system, one might want to construct a "procedure-concatenation" function; currently, there is no way to do this exactly in my system. One could use the B combinator, which acts as concatenation when applied to two procedures; but, the B combinator is also meaningful when applied to things other than procedures, so it does not really cut it as a "procedure-concatenation" function. The primitive this ability boils down to is, I think, a primitive which i'll call "restrict", with this property: "restrict f x" is the same as "x", as long as "f x" is true; otherwise, "restrict f x" is meaningless. For example, "restrict number 2" is analagous to the English expression "the number 2"; it is the same as just "2", since "2" is a number. However, "restrict number Output" would be a meaningless expression, analagous to the English "the number Output", which is meaningless because Output is not a number. Using "restrict" (and certain combinators), it would be possible (using some combinators) to form functions with arbitrarily restricted domains and ranges. This could be useful. By the way, I suspect that a "restrict" could actually be formed in various ways from other primitives... but I won't go into that... > > 1) In a purely-applicative system of the kind I've described, > > it is always possible to replace a sub-expression with > > one of equivalent meaning, while this is not the case > > in the Joy system, because of Joy's heavy dependence on > > a quotation construct. To illustrate, suppose in Joy one > > had a program > > Not true -- as you admit below, the quotation construct has little to do > with it. The problem is caused by being able to treat quotations as lists. > If that were not possible, or if it were restricted in certain ways, there > would be no problem even in this circumstance. Right, it looks like you're not denying that there is this problem with Joy... you're just denying that it stems from the quotation construct... the solution you propose (restricting the ability to treat quotations as lists) is one that the author of Joy has considered; it has the disadvantage that you would then need to have separate constructs for lists (if you still want them). But, as far as I can see, it does look like a remedy that could work. > > foo (plus 0) > > could be freely rewritten as > > foo I > > > regardless of the structure of "foo", if the system had > > realized that the expression "plus 0" means the same > > thing as "I". > > And the exactly analogous expression "0 plus foo" could be freely rewritten > as "foo" in Joy. Hmm... no, my "foo (plus 0)" is not well analagous with Joy's "0 plus foo". The Joy analogue would be "[0 plus] foo". Let me explain why I think this way... Suppose instead that we have "WB (plus 1)"... "WB" is a higher-order function that takes a function, in this case "plus 1" (the add-one function), and yields the function's composition of itself; in this case, that will be "plus 2". Now, could you see that a Joy analogue of "WB (plus 1)" would be "[1 plus] dotwice", where "dotwice" is a combinator that takes a quoted program and does that program twice (such a "dotwice" could be formed as "dup [i] dip i", I think). In contrast, I cannot think of any "foo" such that "WB (plus 1)" would be analagous to "1 plus foo" of Joy. Back to the original example... the Joy "0 plus foo" is a program that adds zero and then does "foo" to the result. It would be analagous to "B foo (plus 0)", the composition of "foo" with add-zero... By the way, "B foo (plus 0)" could probably be rewritten in my system as "B foo I" and then just "foo". > Note that there wasn't even a need for an 'id' placeholder. In the truely analagous example in a applicative system (just given), there is no need for an "I" there either. > Entirely correct. It _does_ cause an impediment; the interesting thing is > that it itself (treating programs as lists) is a form of automated reasoning > about programs. This suggests that the problem it causes _might_ be > fundamental, one of the classic Godel incompleteness problems (as phrased > for concatenative languages, of course). In other words, reasoning about > programs may be easy; but reasoning about programs which are themselves > rasoning about programs is not easy. This is an interesting thought... But, I'm not sure that this (the problem of not being able to substitute equivalent expressions within quotations) is really a fundamental problem; my purely applicative system does not seem to have it, and you've given a remedy that could possibly rid a Joy-style system of it. > > 2) Many fundamental equivalences of Joy programs that one > > might like to show require restrictions, while > > similar equivalences of purely-applicative expressions > > can be taken unrestrained. Specifically, one might > > like to think upon seeing a Joy program of the form > > This is _really_ heavily unfair. Those things aren't "fundamental > equivalences"; they're rewriting rules. Rewriting rules of that nature are > a thing used by syntax-based languages, and you honestly can't expect them > to apply to a non-syntax based one. The fact that some of them do anyhow is > almost amazing. Hmm... I'm not sure what you mean by a "non-syntax based" langauge. All languages (including Joy) have a syntax, do they not? >From this comment, I guess I can see that you just do not value too highly the ability to do "textual reasoning" in a language (i.e., reasoning which is achieved by changing expressions around mechanically, but without paying special attention to their meaning). Yet, textual reasoning (or "formal reasoning" as it is usually called) is something that I find quite interesting; it seems to me to be a very good quality of a language if it can be easily textually-reasoned upon. > The actual rewriting rules Joy has are very simple; they state things like: > > if "t == a b c" and "d e a b c f g == z y x" then "d t f g == z y x". In > other words, direct text replacement is reasonable. Hmm... I'm guessing that that "e" was not supposed to be in the second one, or something, but I get what you are saying... There is some textual reasoning that can be done with Joy, and one of the principles (which you've just illustrated) is that if x is replacable by y then it also holds that a x b is replacable by a y b (this holds for all well-formed Joy expressions "x", "y", "a", and "b", and by "x is replacable by y" I mean "x can be textually replaced by y in any program and the resulting program will make the system do the same thing as before"). > > The analagous equivalence in a purely-applicative system > > is that > > > @@@C f x y > > can be rewritten as > > @@f y x > > This can be accepted without restraint for all expressions > > "f", "x", and "y" of the system. > > That's because combinators in your system have syntactic effect. In order > to do that, I'd have to define: > > C == dip [swap] i; > > [y] [x] [f] C == [x] [y] f Hmm... I suppose you meant "C == [swap] dip i"... but, yes I suppose you could then accept "[y] [x] [f] C == [x] [y] f" unrestrained. But, I don't think this principle is analagous to the "C f x y = f y x", because the former would almost never be used at all in a Joy system at all, while the latter would be very commonly used in my system. On the other hand, a reasoning system in Joy would probably commonly find itself reasoning that "x y swap" is the same as "y x", but it could only realize this once it verified that "x" and "y" were simple programs that just pushed a thing onto the stack with no other effect; this might not be such an awful problem, since a reasoning system would probably want for other purposes information on how many items a program might push onto the stack. > > But, if you can see any awful problems with the approach > > I'd still like to hear, of course. > > The nastiest one I've seen is the lack of ability to *do* things in the > language. You have to add another language on top. That's sort of right. In my system, one would not "do things in the language". Rather, one would use the language to express things that one wants done. This is just a matter of interpretation though, and doesn't really matter at all... You could use my system to get computers to do things, just as you could in other systems. And, it is not necessary to "add another language on top" (this reflects a misunderstanding of my procedure system which I think may have been cleared up now). > I also suspect that even the strength of your functional language, > functions, will be very hard to read and maintain without automated help. > APL functions don't use many combinators, but APL programmers recommend > deleting the program and rewriting it based on the comments rather than > trying to understand the program. Forth programmers don't do that. confession... in working in my CLIP system, on several occasions I've found myself deleting lines and rewriting them after they didn't work on the first try, rather than trying to understand them :-) but, i think this reflects my own unfamiliarity with the system and also the inconvenience of the system's current syntax. > -Billy - iepos ("Brent Kerby") From iepos@tunes.org Thu, 24 Feb 2000 14:15:02 -0800 (PST) Date: Thu, 24 Feb 2000 14:15:02 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Joy, Lambdas, Combinators, Procedures > The system already can't be implemented as a switch-case; > its implementation is like Forth's: > > while next_word(source): > if ( dictionary lookup ) then ( execute ) > else if ( interpret as number ) then ( push on stack ) > else error; Ahh... that's right; I forgot that there was special syntax for numbers. By the way, I'm guessing that a Joy-styled system could do away with numbers as primitives, using instead something like the Church numerals... Combinators analagous to Church numbers in a Joy system I'll call "dozero", "doonce", "dotwice", etc., and in general "iterators". They would have these properties: [x] dozero == [] [x] doonce == [x] [x] dotwice == [x x] [x] dothrice == [x x x] Essentially, each iterator takes a quoted program and yields another quoted program that executes the original so many times. Each of these could be defined in terms of more ordinary Joy primitives as: dozero == pop [] doonce == id dotwice == dup concat dothrice == dup dup concat concat dofour == dup dup dup concat concat concat One interesting thing about these iterators is how easy it is to do arithmetic with them. Multiplication, for instance, is very simple; "dothrice dofour" is the same as "dotwelve", because [x] dothrice dofour == [x x x] dofour == [x x x x x x x x x x x x] You can multiply iterators simply by concatenating them... And then, there is addition, which is a little bit trickier. But, "dup [dothrice] dip dofour concat" is the same as "doseven", because [x] dup [dothrice] dip dofour concat == [x] [x] [dothrice] dip dofour concat == [x] dothrice [x] dofour concat == [x x x] [x] dofour concat == [x x x] [x x x x] concat == [x x x x x x x] In general, it looks like "dup [m] dip n concat" is the sum of "m" and "n". >From this, it looks like a general addition program would be add == [[dup] dip] dip [[i] dip] dip i concat When this is ran with quoted iterators "[m]" and "[n]" on the stack we get [m] [n] [[dup] dip] dip [dip] dip i concat [m] [dup] dip [n] [dip] dip i concat dup [m] [n] [dip] dip i concat dup [m] dip [n] i concat dup [m] dip n concat In turn, when this new iterator (the sum of "m" and "n") is ran with a program "[x]" on the stack, we get: [x] dup [m] dip n concat [x] [x] [m] dip n concat [x] m [x] n concat These iterators seem quite interesting. Exponentiation among iterators is simple... "dotwice dotwice dotwice dotwice" is clearly dotwice multiplied by itself four times (since concatenation acts as multiplication with iterators); thus, in general, "[m] n i" is the iterator "m raised to the nth". Thus we can construct an "exponent" program (taking two quoted iterators off the stack) as: exponent == i i Then, one might wonder how one could achieve subtraction or division with these iterators... My guess is that it is possible, but difficult, as it is with the analagous Church numerals. In Joy, if the ordinary numbers are still available, one can test the size of an iterator by running it on a simple list with exactly one element in it; then test the size of the resulting list, which will tell how big the original iterator was. Oh yeah... one other thing that one might do with iterators is test whether they are zero. It might be possible just to use the "=" program to check, but this probably would not really work, because when applied to quoted programs, "=" only checks to see if they have the exact same structure (as lists), not whether they are equivalent programs in a more abstract sense (for, it is possible to write the zero iterator as several different quoted programs). But, there does happen to be a way to test iterators for zero-ness... consider: [x] [pop [y]] dofour i When this is ran, it gives [x] pop [y] pop [y] pop [y] pop [y] and eventually just [y] All iterators will end up giving simply "[y]" as a result, except for zero, which will give "[x]", [x] [pop [y]] dozero i == [x] [] i == [x] Anyway, I'm not sure what the use of these iterators are... They show what can be done with just the "pure" programs like iterators... These iterators can be used to do arithmetic; but, there are probably better ways, with cleaner solutions for subtraction and division; for instance, one might use a list of bits (perhaps using "[pop]" and "[[pop] dip]" for truth and falsehood), in the ordinary base-2 way; one could then construct addition, subtraction, multiplication, division, and comparison programs in reasonably efficient ways. > > In other words, whereas now Joy has specific support for just a few > > stack-operators like "swap" and "dup", if this new "--" notation were > > adopted in general, then it would not be enough for Joy to support > > just a few of the expressions of the form "...--..."; it would have > > to support them all. > > Actually, it would have to support a finite-length subset of them all. I'm > not a stickler for impossible things. The very fact that I'm only > specifying lower-case alphabetic letters on the left hand side limits the > complexity to 26 inputs, and I'm not interested in more (although I don't > see a reason to limit it to fewer, even though I strongly hope nobody ever > uses that many). Ahh.. You're right. It is not necessary for Joy to support _all_ expressions of the form "...--...". Let me modify my statement to this: "it would have to support so many of them that a switch-case implementation would no longer be practical". But... as you pointed out, it seems that Joy already isn't implemented using a switch on the next word... Anyway, I really don't have anything against adding a "...--..." construct; all I've meant to say is that adding it would be a fairly significant change to the system (it probably would not extend the class of programs that could be written with the system, but it would change the way that the system would need to be implemented). > > I'm still quite interested in knowing of an "abstraction algorithm" > > that could be used to rewrite "...--..." expressions using > > just "dip", "swap", and such. You've hinted that "dip" will > > be important, although surely other operators will also be needed. > > I don't have the algorithm written out, although I'm sure I can find it if I > need to; I've seen it before. I was actually planning on working it out by > hand, since it's not immensely complicated as such algorithms go :-). I would guess that it probably will not turn out to be very complicated. But, sometimes simple solutions can be very elusive (at least, they can be when I'm trying to find them :-)). I bet it would be easier to find a general algorithm if we first try to work out some examples... How about "xyz--zyx"? Well... If I had x, y, and z on the stack, I would probably proceed like this... First, looking at the right side of the "--", I would realize that I need "z" on the top of the stack (for a time). It is already on the top, so I would move on. Then, I need "y" on the top of the stack. "y" is not on the top of the stack, but could be moved there with "swap". So, I would begin the program with swap Then, I need to get "x" on the top of the stack. I've kept track that is still buried two items deep, so it could be moved to the top with "dig2", which is a combinator I made up. I think we may need a whole series of "dig" combinators, so let's see if the series could be constructed from more ordinary combinators. The "dig" combinators would have these properties: a b c d dig0 == a b c d a b c d dig1 == a b d c a b c d dig2 == a c d b a b c d dig3 == b c d a Each "dig" combinator digs up a parameter that is buried so many items deep in the stack. It occurs to me that we could get a stack item to the top by using repeated swapping. Thus we can define the dig combinators as: dig0 == id dig1 == swap dig2 == [swap] dip swap dig3 == [[swap] dip swap] dip swap Each dig combinator can be constructed from the previous by enclosing it in []s and then appending "dip swap". Using these dig combinators, I think I can see a general algorithm for working out "...--..." for combinators that do not duplicate or destroy their parameters (the situation may become trickier for combinators that perform dupping or popping)... here is the "algorithm": program abstraction-algorithm(list leftside, list rightside){ // leftside is the list of letters on the left side // rightside is the list of letters on the right side stack mystack = emptystack; program result = id; foreach x in leftside, push x onto mystack. foreach x in rightside{ where is the letter "x" on mystack now? it is buried "n" deep on the stack, so append "dig-n" to 'result'. adjust mystack so that it reflects this change in stack. } } Applying this algorithm to a few example combinators gives: xyz--zyx == dig0 dig1 dig2 xyz--yxz == dig1 dig2 dig2 xyz--xyz == dig2 dig2 dig2 I think this algorithms always gives correct results, although maybe not good ones. As you can see, the result it gives in "xyz--xyz" is not very nice; we would hope that it would have given "id" (although actually "id" might not be considered a correct result in this case, since "xyz--xyz" might fail when ran on a stack without enough parameters, while "id" would not). The results could always be written in terms of just "swap", "dip", and "id" using concatenation and quotation. Ahh... it now occurs to me a simple, general way to do abstraction... It is similar to the last one, except that instead of moving the parameters with "dig", you copy them with "select", and then at the end you "pop" all the original parameters off. This is the way i guess that most C compilers shuffle parameters around. It reminds of the traditional "SKI" algorithm for combinators, with all its unnecessary dupping and popping. The question remains of how the "select" combinators can be constructed from ordinary primitives like "dip", "swap", and "dup" (a similar, but not identical, question was considered in my last post, so this seems kind of strange). These "select" combinators would have these properties (for example): a b c d select0 == a b c d d a b c d select1 == a b c d c a b c d select2 == a b c d b a b c d select3 == a b c d a These first ones could be constructed in this way: select0 == dup select1 == [dup] dip dig1 select2 == [[dup] dip] dip dig2 select3 == [[[dup] dip] dip] dip dig3 It looks like we can always get a select by first making a copy of the desired parameter (using a dipped dup) and then digging the copy up. But, there is a more direct way to construct the selects without using dig: select0 == dup select1 == [dup] dip swap select2 == [[dup] dip swap] dip swap select3 == [[[dup] dip swap] dip swap] dip swap Since all these select combinators can be constructed from "dup", "dip", and "swap", this shows that all of the Joy combinators (meaning those "...--..." programs) can be constructed from just "dip", "swap", "dup", and "pop". I'm still curious to know if there is a single program that plays the role of "dip", "swap" and "dup" (akin to the "S" of combinatory logic) that, with "pop", can form all combinators. Anyway, it is not likely that an efficient implementation would try to use just "dip", "swap", "dup", and "pop" as primitives; As far as I know, Intel processors do not even have special instructions for "swap", "dup", and "dip"; basing a whole implementation on these primitives would probably be a bad idea. "select" and "pop" would probably be better choices. > > - "f\f f": a function that takes a parameter "f" and > > results in "f f", > > the application of "f" to itself. (This is sometimes called > > the mockingbird combinator). > > - "x\y\x": a function that takes two parameters "x" and "y" (through > > currying) and results in "x". In other words, this combinator > > takes a parameter of "x", and results in a function that > > always returns "x". This is sometimes called the K combinator. > > - "x\y\z\ xz(yyy)(zx)". This is a weird combinator with no common > > name and seemingly little significance. > > Cool. So you also have a compact notation for your combinators (which you > could add to a variant of your language in the same sense that I've > considered adding the stack:() notation to mine). Yes, this way of writing combinators is known as "lambda". In general, "x\Z" means "the function that given 'x', yields 'Z'"; in these definitions of combinators, I am using nested lambdas... For instance, "x\y\x" is the same as "x\ (y\x)", the function that given 'x', yields the function that given 'y', yields 'x'. By the way, although my CLIP system is an experiment to see if systems without lambdas are practical, I really do not have anything against using lambdas in a system, at least internally... (wouldn't that be odd? lambdas used internally to implement a combinatory system...) But, it does look like writing functions in lambda form has some advantages... in particular, when two combinators are written in lambda form, it very easy to tell if they are equal, because in lambda form, there is only one way to write each combinator. (in contrast, when writes combinators in terms of primitive combinators, using application, there are usually several distinct ways that they can be written). For example, consider the combinators "KI" and "CK"... These are actually the same combinator, although this is not obvious from their appearance (this is somewhat analagous to the "[pop] dip" and "swap pop" of Joy, which are the same). However when we write the combinators in lambda form we get KI == (x\y\x) (z\z) == y\z\z CK == (x\y\z\ xzy) (a\b\a) == y\z\ (a\b\a)zy == y\z\ (b\z)y == y\z\z Both reduce to "y\z\z" when they are written in lambda form (actually, we got somewhat lucky that the same names for the variables ended up on both of them; sometimes it may be necessary to rename variables to make them match). But, of course, basing a system on the lambda construct means that the system cannot be purely applicative. This might be somewhat of a disadvantage. It still seems to be a possibility to me that there is some simple algorithm for comparing combinators for equality, without resorting to the use of variables, but I do not know of such an algorithm. Perhaps even there is a certain base of combinators in which there is only one beta-normal way to write each combinator. (any base involving "C", "K", and "I" will not qualify for this, as evidenced by the example of "KI" and "CK", which are in beta-normal form, but which denote the same combinator; a similar example could be used to rule out a system that has both "B" and "C", and several other pairs of common combinators). I don't know... this is actually a question that I suspect is well-studied, so I wouldn't be surprised if the answer is out there somewhere. > > > so all functions can therefore be regarded as combinators in any > > > system (including yours). > > > Not so. But, this is true in a certain sense... there are interesting > > ways in which pure functions can be used to emulate numbers, > > propositions, and lists. It is conceivable that pure > > functions could be used to > > represent even other things like people, pictures, sounds, > > and programs, > > but this would be a bit weird, and I don't plan to do it in my > > system. > > Given the theory behind your system, I don't see how you have any choice. "Output" is a function in my system which is not a combinator. > You *are* using a finite number of things to represent all possible > concepts, surely an infinite set. You _have_ to permit your user to use the > finite number of elements to represent whatever they want. But these elements do not all need to be combinators... > It's like Forth's distinction between words and numbers; numbers are handled > by the compiler, and they're a completely seperate case from words. This is a distinction that is practically convenient, I would guess, but not absolutely necessary. It would be possible to form all numbers from, say, just the primitives "0", "1", "+", "*", "-", "/", etc. (actually, it may not be possible to form all numbers (speaking of real numbers, which are non-enumerable I think, and thus cannot all be represented in any ordinary kind of language), but it is possible to form all numbers that could be formed otherwise using special base-10 syntax, as in "1432" or "23.532"). > > There's one thing somewhat relevant to this that I may not have made > > clear yet... whereas many programming languages only provide > > ways of representing programs, languages of the type I've described > > will cleanly be able to represent other concepts, such as numbers, > > propositions, lists, functions, as well as programs. I think > > some enlightenment may result from comparing my language(s) to > > human languages like English, rather than to programming langauges. > > You're 'accusing' other languages of faults which are your own constructs. > Your language is not the first 'declarative' programming language ever > developed; in fact, it's not even an especially good one, since its syntax > is so focussed on your style of maps. Okay, I can see that maybe this was not such a relevant comment to insert after all... Anyway, here I have not really accused any languages of any faults (I don't consider it absolutely a fault not to be able to represent anything other than programs). Other than that, I can buy your reply... Indeed my language is not the first "declarative programming language" or an especially good one. > You've also invented a concept of "true representation", in which your > language is gifted with the ability to truly represent things, while all > other languages falsely represent them. Hmm... Where have I invented the concept of "true representation"? I really have not said this. > Since you've never defined your > concept of representation, it's impossible to guess what this means to > anyone. Indeed, I probably should not of brought up the matter of the "meaning" of expressions, since it doesn't really matter. It would be possible to formally assign each expression in a language a specific corresponding meaning, but I don't really plan to do that in my system. who cares what the meaning is, as long as it works... > > Neither do I have my mind set on any particular > > technique for interpreting text that user's type in. Anyway, it > > doesn't really take very long to parse text, so this doesn't > > really seem to matter too much to me > > It's certainly much, much more than the 1*O(n) operation my language > requires. Hmm... I believe the order of my parser is also "O(n)" I guess you might say, if "n" is the number of characters in the program. I think this could be proven by examining the fragments of C code in my last post. > Justification: each word in the input stream has to be lexed, then each word > has to be reduced to its internal representation (which is likely a matter > of a dictionary lookup and replacement with the appropriate number of "S"es, > "K"s, and other fundamentals). Then you have to load the words into their > tree structure, reading left to right and performing pinhole optimization. > You use a stack to keep track of unresolved applications, since the most > recent application "@" you reached is always the appropriate place to attach > the next parameter (even if the parameter is itself an application). Not sure what "pinhole optimization" is... Incidentally, I do use a stack in my normalizing procedure, norm_head(), but I didn't consider this part of the parser; there is actually no limit to the time that will take. My current normalizer is very slow; I realized recently that there is no sharing of reductions for duplicated paramaters (this is lame); it will have to fixed... but there will still be no limit to its slowness. Normalizing combinatory expressions is I think a task that is fundamentally indefinitely slow (there exist fairly small expressions that take massive amounts of reduction before they can be normalized); it might not even terminate at all. > Finally, you can optimize (a task which I'll assume takes zero time), and > then evaluate the result, using at least depth first traversal (assuming > that you only want to execute it as a program). > > The biggest time-sink there is the depth-first traversal; it's n*log(n). > That's not as bad as I'd feared. Of course, adding the possibility of > source modification we get a new worst case of n^2 log(n). I'm not sure where you are getting these numbers from... I would be happy if in principle my normalizer terminated in about "O(2^n)" time if "n" is the size of the expression fed to it; but this is not so... I think there are series of expressions (with linear increase in size) whose slowness in normalization grows even worse than exponentially. Actually, such a series is WBII (4 steps) WB(WB)II (12 steps) WB(WB)(WB)II (60 steps) WB(WB)(WB)(WB)II (258294 steps) WB(WB)(WB)(WB)(WB)II ... In each case, the expression normalizes simply to "I". The slowness can be expressed this way: if one expression takes "n" steps to normalize, then the next takes approximately "2 ^ n" steps. I am not sure how many steps that last expression would take to reduce (my little normalizer segfaulted, but probably would never have gotten finished in my lifetime anyway, even with unlimited memory); there may be a simple formula stating how many steps each takes to reduce, but I do not know what it is... Anyhow, I know that the next expression in the series not listed would take more than "2 ^ 65536" steps. But, this is really only an indication of the power of a combinatory normalizer. A combinatory normalizer can be used to solve any computational problem, I think... > The problem with your metaprogramming suggestion is twofold: > > - it requires knowledge of the source file's name and total overall > structure, as well as the willingness to muck about with the original source > code. It requires knowledge of the source file's name, yes. Normally when you want to refer to things, you have to know a name for them. But, it is possible, of course, for a program to refer to its own source code without using its name. It could be done using Quining (an example of this will be given shortly). Now, if the program wanted to refer just to itself, rather than to its source code, then the problem is very simple. Just use the "Y" combinator... this is very useful anyway when constructing indefinitely repeated procedures; for instance, as mentioned already, the program that outputs "hello" and then goes back and "does itself" again is "Y (Output 'hello')". It would of course be possible for a program to do other things with itself than just execute itself. For instance, the program, Y (Output 'hello' . C Output stop) is a program that Outputs "hello" and then Outputs the program itself (I'm supposing here that this "Output" takes an arbitrary parameter, not necessarily a string, and outputs a representation of it). By a few reductions (including "Y"'s reduction rule, which says that "Yf = f(Yf)") this program is the same as Output 'hello' (Output (Y (Output 'hello' . C Output stop)) stop) The interesting thing is that the Y combinator can actually be constructed from other combinators (for instance, as "BM(CBM)", or in other words, "B(WI)(CB(WI))"). An example application of such a constructed Y: BM(CBM)f M(CBMf) [B] M(BfM) [C] BfM(BfM) [M] f(M(BfM)) [B] f(BfM(BfM)) [M] f(f(M(BfM)) [B] f(f(BfM(BfM))) [M] f(f(f(M(BfM)))) [B] f(f(f(BfM(BfM)))) [M] f(f(f(f(M(BfM))))) [B] ... It goes on forever, the reduction does of course. It reminds me of a puffer from Life. recursion is a weird thing. Ah... back to quining... one might construct a pseudo-quine in my system as Y Output But, if this would not really be guaranteed to output its exact source code; it may output some other representation of the program. Also, it is sort of cheating to use such a complex primitive as Output (which sort of has built-in stringification). But, it is possible to construct a real quine using just "put" (a function that takes a string and yields a procedure that outputs that string)... here is a real quine: (put + K(put qmark) + put + K(put qmark)) "(put + K(put qmark) + put + K(put qmark)) " This relies on sugar for quotation, but there are other (messier) ways of doing it if that is not available. > Note the difference from how Forth does it: > > - Forth only allows metaprogramming in the part which hasn't been parsed > yet. This means that re-reading is never needed. > - Forth allows modification relative to the current location in the source > file. > - Forth's modifications have zero effect on the source itself; they don't > even affect the internal representation of the source. Technically, all > they do is discard source and/or provide alternate source. Hmm... I can see that the way Forth does it is not the way I planned on doing it. It seems that in Forth it is possible for a program to open up a file (or some other field) containing its own source code and change it so that when the parser gets to it, it will something different than it would have if it had not been changed. This is really weird. I have no idea why I would want this ability. On the other hand, in a finished system, I would hope that a program could change its own internal representation in memory, in a way that could make it go faster. This probably would not be a thing that one would ordinarily do explicitly in my system though. Such things would ordinarily be handled by the core of the system, which handles memory management, compilation, and such, and which moves things around and changes them whenever it wants. > -Billy - iepos ("Brent Kerby") From alangrimes@starpower.net Fri, 25 Feb 2000 15:07:27 -0800 Date: Fri, 25 Feb 2000 15:07:27 -0800 From: Alan Grimes alangrimes@starpower.net Subject: Languages. I think Tunes should be written in Assembly (or forth). As we figure out what we're doing we can start abstracting it untill we get a full-fledged HLL, but in the mean time I vote for ASM or FORTH. -- Language is the calculus of consciousness. http://users.erols.com/alangrimes/ From dem@tunes.org Fri, 25 Feb 2000 12:48:55 -0800 (PST) Date: Fri, 25 Feb 2000 12:48:55 -0800 (PST) From: Tril dem@tunes.org Subject: Languages. On Fri, 25 Feb 2000, Alan Grimes wrote: > I think Tunes should be written in Assembly (or forth). > As we figure out what we're doing we can start abstracting it untill we get a > full-fledged HLL, but in the mean time I vote for ASM or FORTH. Shouldn't we know what we are programming before choosing a language and starting to write code? Also, why does a whole project have to be in one language? It's fine to argue for ASM or Forth, but what aspect of TUNES design is suitable for implementation in them? -- David Manifold http://bespin.dhs.org/~dem/ This message is placed in the public domain. From iepos@tunes.org Fri, 25 Feb 2000 16:08:20 -0800 (PST) Date: Fri, 25 Feb 2000 16:08:20 -0800 (PST) From: iepos@tunes.org iepos@tunes.org Subject: Procedures in CLIP (was: Joy, Lambdas, ...) > > But, what action does "Square a number" express? > > What would the system do if you told it, "Square a number". > > Square a number, hopefully. Possibly by multiplying it by itself, although > I didn't specify that. What would you expect it to do? > > Hmm, if you really wish to be literal, I would expect it to take a > representation of a number as input, and produce a representation of the > square of that number as output. > > I'm not sure what blocks you from understanding the need for that, but > perhaps it'll help if I use a more complicated example: "Compute the DES > encryption of a 8-byte quantity." The two procedures express the same basic > idea. Ahh... This example did help. Thanks. Such a program could be represented in my system... Reverting back to the simpler example of squaring, a program to compute the square of 129 could be written as something like: put (stringify (square 129)) This seems to me like a natural way to express the procedure, since what you really want the system to do is print onto the screen a string representing (in some normal form that the user can understand) the square of 129. If you told the system just, square 129 it would respond with an error, complaining that this was not a procedure and that it could not execute it. Anyway, once you told the system to "put (stringify (square 129))", the thing it would set work to do is to get "stringify (square 129)" into a form that it can understand well enough to actually output the string. It would probably then discover that this could be achieved if it could get "square 129" into a normal enough form. Actually, "stringify" is sort of an odd primitive that I have not really thought much about... As I've used it in my example, one might hope that when applied to the square of 129, it would yield the 5-character string "16641". Surely such a "stringify" cannot be too fundamental though, if it has such a special connection with base-10 number representations. Probably, in a real system, I would not use a "stringify" quite like this. An alternative would be to use a procedure instead, a procedure that takes a thing as a parameter and results in a string representation of that thing; the system would probably pick a representation that it thought the user would like... I'm not really sure how this might work... > > A function, as I use the word, is just an abstract mapping from > > keys to values... not a step of a process... > > "Mapping" is a form of a verb, "to map". In other words, a mapping may be > used as part of a procedure to map one thing to another. A mapping is a > noun; a procedure is (essentially) a verb. Hmm... certainly functions could be used to construct procedures... Actually, every function has an associated procedure, a procedure which takes a single parameter, has no effect, but then results in the function of that parameter (procedures like these, that have no effect, in general I will call "pure procedures"). There is a precise connection between functions and their associated procedures; however, functions would be distinct from their associated procedures in my system ... There is probably a little bit of confusion now associated with the word "procedure", since I have used it in several distinct ways... I originally used "procedure" to refer only to specific things-to-do. A specific thing-to-do I will now call a "command" (this use of "command" is similar to what I called "procedure completions") while a "procedure" I will use to refer to things that are like commands, except that they take parameters and yield results. > > > Let me try to write a brief program: > > > > & > > > ! @exec @Output @stringify @@+ 4 5 > > > stop > > > This is not correct. "&", "!", and "exec" are all strange things that > > you do not need. Also, "stop" is not needed in this example. If I > > understand what you want the program to do, it should be written: > > > @Output @stringify @@+ 4 5 > > > This is a procedure that outputs the string "9". There is no need > > to wrap it with "exec" or "&". When you type in an expression > > representing a procedure, the interpreter will probably respond > > by executing the procedure, and then giving you another prompt. > > In other words, the interpreter implicitly prepends the "&" operator when > appropriate. There will be times when you will need to prepend it yourself. > (I don't care what the operator looks like, I'm just using a handy key.) Well, I guess you could think of it that way if you want, although my current implementation does not prepend anything like an "&" operator to expressions. > This is different from what would happen if the user typed "@@+ 4 5"; in > that case, I would expect the interpreter to cleverly realize that the user > probably wanted the system to perform the map thus indicated (even though > it's not a procedure and thus has no meaning in terms of being performed). If a user typed in "@@+ 4 5", the system would give an error because it does not denote a procedure... one might suggest extending the system a sort of sugar so that the system would just write the normal form of the user's input if it was not a procedure; but, I would not like the system this way, as it would probably make programming more error-prone; I would not mind leaving explicit the instruction to output. > The problem is that I'm trying to think of your system as being > consistent and simple, when it's actually not. It's got a very complex > interpreter, which tries its best to "do what you mean". The thing is, this > actually makes sense for your language, since your language isn't about > _doing_ things; it's natural that your interpreter has to be smart in order > to _do_ things using it. This is exactly correct. > Okay. Sorry for the visible thinking process. I think I understand the > reason for this; I'm missing one little thing, though, so I'm going to try > repeating what you said. > > "@Input someFunction" means to get some input, apply someFunction to it > (resulting in a procedure completion), and then execute the resulting > procedure completion, right? Yeah... that's right. The expression "@Input someFunction" denotes the command ("procedure completion") of getting an input and then going on to do the command "someFunction foo", where "foo" is the input that was gotten. > So in a sense @Input is a REALLY special > function, more like a combinator, right? Yeah... "Input" is a fairly special function. I wouldn't consider it nearly as SPECIAL as some combinators like I, B, C, S, and K though :-) > Okay, so at this point it's pretty clear that "someFunction" has to be > _executed_ at runtime; unlike the other functions we've been using, it's > _forced_ to act like a procedure. And who is the one to apply the FORCE? I do not see the problem that apparently you see. Note that it is not "someFunction" itself that is executed (it would not make sense to execute a function). Rather it is the application of "someFunction" to the "foo" input; this application is supposed to be a command ("procedure completion"); if "someFunction" turns out not to be a function from strings to commands, then the system would never have began executing "@Input someFunction" in the first place, and would have complained that it was jibberish (actually, this is not true of my current system, which would not complain until after it had read the input and tried to execute "someFunction foo", but some future revision of my system should work this way). > I don't like this: I think that your > model of input and output needs to be refined. It just doesn't seem to work > with the rest of your ideas. Perhaps one of the other applicative languages > might provide a useful theory? 8-( Really I see no problem with this model of Output and Input... Actually, I have not even implemented Output and Input in my system. All I have is a raw "putc" and "keyhook". But from these I was planning on implementing an Output and Input at some point. > Oh, and one more question. Suppose I wanted to input a number and print the > number which is five more than it. Let @Input handle numbers only (so that > we don't have to worry about errors). > > @Input @Output @+ 5 > > Is that right? No, unfortunately, this would be jibberish to my system. Perhaps it would help clarify things if I state the "types" of some my primitives: 5 : Number + : Number -> (Number -> Number) Output: String -> (Command -> Command) Input : (String -> Command) -> Command For example, the function "+" takes a Number and yields another function, an adding function, which is a function from a Number to a Number. You could of course also think of it as a function that takes two Numbers through currying and yields a Number. Now, for the Output... This function is only meaningful when applied to a string. Your expression includes "@Output @+ 5", an application of Output to a function; this is not meaningful. Also, it seems that you are trying to apply "add-5" to a string; the Output and Input procedures as I originally thought of them were simply procedures that outputted and inputted strings... If you were going to take a result of Input and add a number to it, you would first have to decode the inputted string using a primitive which I'll call "meaningof" (which is sort of the inverse of "stringify"); then once you had your result as a number, you would have to "stringify" it before outputting it... However, for the purpose of this example, we can modify Input and Output so that they input and output things in general, rather than strings (they have "meaningof" and "stringify" built into them, so to speak). Their new types would be Output: Thing -> (Command -> Command) Input : (Thing -> Command) -> Command Now, using this Input and Output, the program you wanted could be constructed as: Input (C (Output . +5) stop) Recall an example from an earlier post, which just inputted something and outputted it back: Input (C Output stop) The new program which adds 5 first is the same as this, except that "Output" is composed with "+5" (the add-5 function). In that "@" syntax that program could be written: @Input @@C @@B Output @+ 5 stop Once an input "foo" is gotten, the system goes on to execute C (Output . +5) stop foo which reduces to (Output . +5) foo stop and thus Output (+5 foo) stop > If Joy were subject to the same restrictions, the program would look like > this: > > [5 + Output] Input. > > Do I get it? Yes, I think this is right, although this would be a weird thing to do in Joy, of course... One would probably really use Input 5 + Output > Another possible example would be > > [5 swap / Output] Input. I'm not sure what is meant by "/" here... > > @Input @@C Output z > > Hmm. Yes, I think I see this. And then if we write the result of Input as > "Input", we get: > > @Output "Input" z > > Thanks to the 'swap' action of @@C. Right? Correct. > > I'm guessing this may still seem a bit foggy, as it is not > > entirely familiar even to me, who has made up this system. > > Good call. > > > Now for another example... Now suppose we have more general > > primitives "Rinse", "Scrub", and "Dry", which are functions > > (taking a piece of dishware or something) that yield procedures; > > these procedures would be kind of like the "Output" mentioned > > earlier, which is also a function that yields a procedure. > > Now, with these three primitives, we want to form a function > > that takes a piece of dishware and yields a procedure that does > > all three things to it; using a lambda, we could express this > > procedure as > > Note that lambda actually does a poor job of modelling this action; the > problem is that lambda notation implies that the object being named remains > constant within a given lexical scope, and an object which is at one moment > 'wet' and at the next 'dry' is not constant. Uh oh... I can see that using a physical example was a bad idea. I had thought of Rinse, Scrub, and Dry as simple procedures with no results. I had thought of the "piece of dishware" as the abstract "identity" of the dish, not the state or the physical structure of the dish, which changes over time. Another example may help clarify this; consider this command here written in English: Send Bill the letter. Then, wait until Bill sends back a response. Then, throw the response in the trash. Does it seem unnatural that the command makes several references to "Bill"; would it have been clearer if the author had written "the new Bill" in the second occurence to make clear that Bill may no longer be the same? It seems to me that it would not make it clearer; the two references to "Bill" refer to the same person... My multiple references to a piece of dishware are similar to the multiple references to Bill in this silly command. > Hmm. I can't figure out how to have a function take a parameter which is > returned by another function, and then return something which is used by yet > another function. It sounds like you have three functions, and want to yield their composite. If your three functions are called "f", "g", and "h", then the composite could be called either "B f (B g h)" or "B (B f g) h", since "B" is associative; or you could just write it as "f . g . h", using sugar... > Okay, after a weekend of relaxation it makes a little more sense. It's > merely a matter of convention -- we have to choose which order the > parameters will go in. I choose to put the parameter list before the return > function. > > @@Rinse dish Dry Okay... it took me a bit to figure out that you were redefining "Rinse", "Scrub", and "Dry" for the purpose of another example... I see now... If I understand, you want your "Rinse" procedure to take a dish, and then result in the new dish (the new "rinsed" dish, as if it were distinct from the original; I guess we are now talking about the states of the dishes, so to speak, rather than the "abstract identity" of the dishes). The "Rinse" primitive itself would then be a function of the type Dish -> (Dish -> Command) -> Command Essentially, "Rinse" takes a Dish and a continuation that uses the new Dish. > Hmm, it seems to me that multiple returns would be easy to perform (or at > least would be no harder than single returns). Your opinion? I'm not sure I quite understand this. By "multiple returns", are you talking about returns of multiple parameters? > Finally, it's clear to me that you'd need a combinator to handle anything > more than two levels of that. What combinator would that be? How would you > Wash, Dry, then Rinse a dish using this design? Well... if you really want to rinse the dish after you've dried it, then you could do it this way (where "foo" is the original state of the dish): Wash foo (x\ Dry x (y\ Rinse y (z\ stop))) This program, after it has washed, dried, and rinsed the dish, goes on just to stop, disregarding the final state of the dish. A variation of it could be written that passed on the final state to some other function. Now, that last program could be written without lambdas as: Wash foo (x\ Dry x (y\ Rinse y (K stop))) Wash foo (x\ Dry x (C Rinse (K stop))) Wash foo (C Dry (C Rinse (K stop))) If you wanted to abstract out the dish "foo" to be washed, you'd get: C Wash (C Dry (C Rinse (K stop))) Finally, if you want to abstract out the thing to do after the job is finished (instead of necessarily "stop"), you'd get: C Wash . C Dry . C Rinse . K > > For curiosity, a program analagous to "Rinse + Scrub + Dry" > > I think would be written in Joy as > > > dup dup Rinse Scrub Dry > > Ideally, it would be written (using my design) as > > Rinse Scrub Dry. Ahh... this would be a variation that kept the final state of the dish on the stack. A similar variation could be made of mine: C Wash . C Dry . C Rinse Right now it is standing out that with my style of procedures, it is harder to express this example. I think I may have pinpointed why this is so; it is because my approach is fundamentally Backwards and needs to be fixed (I'll clarify on this in a minute). > However, using your design, your writing works; I would more commonly write > it as > > dup Rinse dup Scrub Dry; > > since we only duplicate before the use. Makes sense to me. Yeah... it's fine with me that way too. > Note that in my design, 'Dry' returns the dried plate, so the whole function > returns a clean and dry plate. Your function doesn't return anything. My > function can be modified to act like yours by rewriting it, intuitively > enough, as follows: > > Rinse Scrub Dry drop. Yes... I see that, if "drop" is a synonym for "pop" (is it?). > > Anyway, do you understand now the way of doing procedures that > > I've described? (If you do, you might try to give me another > > example program)... > > How did I do? You did well... it looks like we're communicating now. > > It seems to be quite a nice, pure approach > > that, as far as I know, has never been attempted (of course, it > > may have been attempted and I not know). > > I don't really like it very much, honestly. 8-( > It may be pure, but I fail to > see any benefits this purity brings. Joy is equally pure, but is _much_ > easier to work with procedurally, I suspect that my kind of system may lend itself better to textual reasoning, the primary fault of Joy being the "opaqueness" of the quotation construct; but, as you've said, this problem of Joy could be remedied... But, I think I may be close to understanding the exact relationship between my kind of system and Joy's... If the exact relationship can be discovered, it will be easy to judge which is superior, or if they are really just simple sugared variants of each other. > mainly because Joy is theoretically based > on composition, and procedure execution has the same behavior as function > composition. My approach is also theoretically based on composition (although composition does not play quite so fundamental a role as it does in Joy). > Whoops! I was just checking my Python URLs for the week, and I read the > explanation of Continuations at > http://www.ps.uni-sb.de/~duchier/python/continuations.html; guess what? It > turns out that your procedures match the formal version of the theory of > continuations. Cool. I've used continuations before, but I haven't seen > them explained that way. Take a look. This pointer was very very helpful. I also took a look at several other pages on continuations. I had heard the term "continuation" before somewhere but did not realize that it was identical to the technique which I used. It is a bit of a surprise that this technique is used in common optimizing compilers (on the other hand, it sort of is not surprising that continuation passing is used in optimizing compilers, considering what an amazingly Good technique it is). Now I'll share with you my some new (untested) enlightenment... All this time with primitives like "Output" and "Putc" I have been having them take the continuation as their final parameter (after the string in the case of Output, or after the coordinates and the character, in the case of Putc). I now realize that the correct way is for the continuation to come First in the parameter list (I know this sounds like a terrible, doctrinaire statement, but bear with this and you will see that it really is Better this way). I will now redefine what I mean by "procedure" (this is not consistent with my prior usage, but who cares): a "procedure" is a function that takes a continuation parameter and possibly several other parameters afterwards, and yields a command that that does something and then goes on to do either the continuation, or else an application of the continuation to something else, or else an application of such an application to something else, or else ... (what we're trying to say here is that the continuation may be applied through currying to as many things as the procedure wants to apply it to, although the number of such things must be the same each time). The most immediate advantage of having the continuation parameter come first is that we can now identify the Regular Combinators as a kind of procedure (albeit a "pure procedure", which has no effect except to filter the parameters). I have for a while suspected that the combinators "C", "W", and "K" were analagous to the "swap", "dup", and "pop" of Joy respectively; I think now I can see why this is so: C f x y == f y x W f x == f x x K f x == f You can see that the rewrite rules for these combinators are quite similar to the rules that Joy uses for "swap", "dup", and "pop", except that these seem to have an extra parameter "f" at the front that was not present in the Joy analogues; this parameter is essentially the continuation parameter... The "C" combinator, for instance, can be thought of as a procedure which takes a continuation parameter "f" and then two other parameters "x" and "y"; it goes on to do just "f y x". Essentially, the "C" combinator, if you think of it as a procedure, takes two parameters and has two results, the two results being the exact same as the two parameters except that they are swapped. There is also the "B" combinator... B f x y == f (x y) As a procedure, it takes two parameters and has one result, the application of the first parameter to the second. Although "B" is used in many cases in my system where "dip" is used in Joy, I do not think they are as closely related as C, W, and K are to "swap", "dup", and "pop". Also, there is one very fascinating thing which I just realized about the "M" combinator (which takes a function and applies it to itself, and is constructable as "WI" or "SII"), M f == f f This combinator can be interpreted as a procedure that takes no parameters, but which results in the "current continuation". This could be very useful for the purposes of iteration; I'm not really quite sure now what the exact relationship is between this "M" though and "Y", which can also be used for iteration. I'll have to think on this some more... Another combinator which will play quite a special role in my system, I suspect, is the so called "T" combinator (constructable as "CI"): T x f == f x Note that "T" is not a regular combinator, since its first parameter is not used as a continuation... However, "T" can be used to form procedures; specifically, "T" can be used to form a procedure that takes no parameters and has no effect, but which has a single result. For instance, "T foo" is a procedure that immediately results in "foo"; when "T foo" is applied to a continuation "f" it results in simply "f foo" (by T's rewrite rule). Thus, "T" can be used to form procedures that "push a value onto the stack", so to speak (although in a way there really is not an explicit stack). Now, here is a simple program that we could write using this new convention of the continuation coming first: T "hello" . Output If you are used to Joy, you would probably think of this program as a program that pushes "hello" onto the stack, and then runs Output. But, you could also think of it more literally as a function that takes a continuation "f" and yields (T "hello" . Output) f T "hello" (Output f) [B] Output f "hello" [T] The thing we have then is a command that outputs "hello" and then does the continuation "f". The original program might also have been written as C Output "hello" It is fairly well known that in "Tx . f" is the same as "C f x" in all cases. Now, for another example, using "putc": T 'x . T 5 . T 4 . putc This is a program that pushes "x", "5", and "4" onto the stack and then runs "putc". You could more literally think of it as a function that takes a continuation "f" and yields T 'x (T 5 (T 4 (putc f))) [B] T 'x (T 5 (putc f 4)) [T] T 'x (putc f 4 5) [T] putc f 4 5 'x [T] Note that the order of the parameters has been reversed; the "T" combinator naturally has a reversing effect... Note that this program written as "T 'x . T 5 . T 4 . putc" is quite analagous to how one would write it in Joy, as: 'x 5 4 putc Now for another example... The program which takes an Input and immediately outputs the result back out: Input . Output Again, this is quite analagous to how one might do it in Joy... This is a function that takes a continuation "f" and yields (Input . Output) f Input (Output f) [B] This is a command that gets an input, and then goes on to do "Output f foo", where "foo" is the input that was got. In contrast to my other example program that did the same thing, it is now not necessary to use "C" (it was only necessary before because the "Output" was Backwards). Now for lists... lists are very interesting things, which might be interesting to discuss, since they play such a fundamental role in Joy, and since I'm trying to find the connection between my system and Joy... Although it would be possible to take lists as primitive in my system, and have special constructs for "cons", "car", "cdr", and such, there is an interesting way in which lists can be emulated with something else. In particular, a list can be emulated using a procedure that has no effect but which pushes some items onto the stack (again, take my references to the "stack" loosely). For instance, the list of the numbers "3", "4", and "5" (in that order) could be represented as the pure procedure f\ f 3 4 5 This function takes a continuation "f" and yields the application of that continuation to "3", "4", and "5". There are ways to write this function without using a lambda; one way is C (C (C I 3) 4) 5 Another way is T 5 . T 4 . T 3 In fact, "T" can be used to form a unit list, and composition can be used to concatenate lists (albeit in a backwards way; the first list comes last, and the last comes first). So, in writing the list this way, we have wrapped each element of the list with "T" to form unit lists and then concatenated them all together to make the big list. You can see that this list when given a continuation "f" gives (T 5 . T 4 . T 3) f T 5 (T 4 (T 3 f)) [B] T 5 (T 4 (f 3)) [T] T 5 (f 3 4) [T] f 3 4 5 [T] I think I can almost see a direct way now that Joy procedures can be translated into procedures of my system. I think I'll give it a try... First I'll start with a simple example: the Joy program "3 2 + put", which outputs "5". That program would be written in my system as: T 3 . T 2 . T + . B . B . put You can see that this program when applied to a continuation "f" will give: (T 3 . T 2 . T + . B . B . put) f T 3 (T 2 (T + (B (B (put f))))) [B] T 3 (T 2 (B (B (put f)) +)) [T] T 3 (B (B (put f)) + 2) [T] B (B (put f)) + 2 3 [T] B (put f) (+ 2) 3 [B] put f (+ 2 3) [B] This is the program that puts "5" and then goes on to do "f"... If one were really going to write this kind of program in my system, one would probably use something like T (+ 2 3) . put In the first version, the "+" operator of Joy was translated into the "T + . B . B" of my system, since the "+" of Joy sort of has two applications built into it. Here are a few words of Joy and their translations to my system: swap: C dup: W pop: K put: put get: get 3: T 3 +: T + . B . B Now for the matter of quoted programs... A quoted program in Joy is a simple program that does nothing except push a single result onto the stack. The way I'll translate quoted programs in my system will reflect this... A quoted program like "[3 2 + put]" will be translated as just "T (T 3 . T 2 . T + . B . B . put)"; basically it is translated the same way as the program within the []s, but with "T" applied to the whole thing. Now, here are some more Joy words and their translations: i: T dip: CB concat: TB . B . B cons: C . B . B For curiousity, the Joy program "dig3" or in other words [[swap] dip swap] dip swap is straightforwardly translated into my system as T(TC . CB . C) . CB . C Do you see how "[swap]" has been changed to "TC", and "dip" to "CB", etc.? You can see that it works in analogy with "dig3" by seeing that when it is applied to a continuation "f" and four other things, you get (T(TC . CB . C) . CB . C) f x y z u T(TC . CB . C) (CB (Cf)) x y z u [B] CB (Cf) (TC . CB . C) x y z u [T] B (TC . CB . C) (Cf) x y z u [C] (TC . CB . C) (Cfx) y z u [B] TC (CB (C (Cfx))) y z u [B] CB (C (Cfx)) C y z u [T] B C (C (Cfx)) y z u [C] C (C (Cfx) y) z u [B] C (Cfx) y u z [C] Cfx u y z [C] f u x y z [C] As you can see, it is a procedure that that takes four parameters and results in those same four parameters, but with the "u" (which was buried three deep) dug up to the top. Isn't this weird... ? By the way, that last "dig3" in my system I would not normally write that way. Normally it would be written: C . BC . B(BC) Also, I think I might have some trouble constructing certain operators on lists... for instance, the "length" operator which, given a lists, gives its size... I'm not sure how I would construct it... It would be possible, though, if I had a "reification" primitive or such... But, the solution I usually use is to make the first element of a list contain its length in some cases... I'm not sure if this is good or not, though... Another option would be to construct lists as chains of pairs, with a terminator; this is the approach that is usually used, I guess, but it would make the other list operators (for instance, concatenation) much more complex. > Start is a non-function. That is, its only purpose is to do something, not > to 'return' something. > > Right? This is true, in that "start", as I've defined it, is a procedure that does not give a result. But, in a literal sense, "start" is a function, a function that takes a continuation and a task to start, and yields a command (a continuation). > > of itself, for instance. Also, the system might do common-sense > > consistency checks before accepting new information at all, to make > > sure it is not absurd; but, a full consistency check (meaning a > > proof that the added information will not cause everything to > > be provable) in general would not be possible, I think, according to > > "Godel's theorem", unless the system is already blown > > (in which case everything would be provable, including > > that the system is consistent). > > Well, you _can_ check that the new theorem doesn't contradict the axioms, > can't you? Well, I'm hardly a specialist on this, but if I understand correctly, you cannot. You could, of course, check that the new theorem isn't a direct negation of any axiom... But, checking all the _consequences_ of the axioms, to see that none of them is an absurdity, is generally not possible. A brute force approach will not work, because there are an infinite number of consequences of the axioms, and you cannot check them all. But, there are some cases where the consistency of a system can be shown, but it is usually quite a difficult task, and there is not a general algorithm for doing it. However, I think Godel's theorem only applies to a certain class of systems (albeit quite a broad class). You could maybe get around it if, for instance, you arbitrarily restricted your proof system so that only formulae that could be proven within 20 steps using the axioms were accepted as theorems. Then, a brute-force check for consistency would in principle be possible (although probably not practical). Anyway, I wouldn't let this theoretical limit stop you... A partial consistency check (that only looked ahead some certain number of steps to see that an absurdity could not be derived) would in most cases be acceptable, I would guess. Some inconsistencies could slip through, but only those of quite a subtle kind... There would probably from time to time be cases where the system found an inconsistency... I'm not sure what would be a good thing for it to do at that point... One possibility would be for it to eradicate the absurd result from its memory; then, the least trusted of the premises from which it was derived would also be eradicated; then, if this one was derived from somewhere, its least trusted premise would also be eradicated. This would continue until an axiom was reached, one of the least trusted of the axioms upon which the original absurd result was based; this axiom would be eliminated. Anyway, I'm still open for ideas on how do a logic system... I suppose there are real automated logic systems out there (for instance, Coq or HOL) which I could learn from... > > - "iepos" (Brent Kerby) > > -Billy - "iepos" (Brent Kerby) From btanksley@hifn.com Mon, 28 Feb 2000 12:09:52 -0800 Date: Mon, 28 Feb 2000 12:09:52 -0800 From: btanksley@hifn.com btanksley@hifn.com Subject: Joy, Lambdas, Combinators, Procedures > From: iepos@tunes.org [mailto:iepos@tunes.org] > this message is still way too long, so i'll skip through some things > (I read them of course, but just do not have time to reply to them), > but I'll at least try to address your direct questions. I'll try to do the same trimming -- you're better at it than I, though. > > > I still do not quite get exactly what you mean by > > > "parameter counts". > > For example, in your language, is this an error? > > @@@ackermann 4 5 6 > > (Ackermann is a two-parameter function.) > That would be an expression that would probably have no denotation > under the language (although it is "well-formed", in a certain sense). > Any expression involving this expression would also have no > denotation. > I guess you could say that it is an error. If you fed this > expression (or an > expression involving this expression) to an interpreter, > it would probably complain that the expression does not denote > a procedure (the interpreter can only execute expressions > that denote procedures). Yes, this is exactly what I mean. Your language has to, at some point, count parameters before execution. This is a natural extension of the fact that your language is "all about" binding parameters to functions. It is interesting, though, that your language can express "overbound" functions. I don't understand what that could mean to the function -- do you have any ideas? If we can come up with a theoretical functional meaning, perhaps we can make sense of them procedurally as well. In general, currying takes a function of n variables and a parameter, and returns a function of n-1 variables. I don't see how you can get away with disregarding the terminating condition: when n=0, currying is impossible. I'd like to hear a method, though; perhaps it's reasonable to simply ignore those extra variables. That _does_ make sense, pragmatically. It's an irritation when coding, though, since you couldn't tag a large class of simple errors. Perhaps counting parameters is the job of the correctness checker, not the executor? Correctness checking is very complicated anyhow, and adding this tiny bit of complication to it won't upset it. I would be interested in hearing any other solutions you have, though. > > Look up K-complexity on the web -- it's the theory of > > reducing functions to > > just a few combinators. (Your compiler could theoretically > > work that way.) > > The problem is that it's a very hard task; > Hmm... I'm not familiar with "K-complexity", but this is incorrect, > I think. It is really quite an easy task to rewrite any combinator You're right; I was wrong. It's not a trivial task, and it expands rapidly; but in its naive form it's linear. Optimization is where it gets hard, but optimization is obviously a difficult task for concatenative languages as well. > Well, I guess usually "imperative" is used to describe systems > in which the "command" or "procedure" play a fundamental role, > whereas "declarative" is used to describe systems where the > "statement" (expression of information) plays a fundamental role. Hmm. Not really, although that's a possible expression of it. A better way to express the difference is to point out that imperative languages express sequences of commands, each of which has some kind of effect on its environment. Declarative languages express -- or pretend to express -- unchanging facts about reality. Joy contains a declarative language: its definitions are an imitation of the notation the author uses for expressing rules, and contain restrictions which allow the compiler to check them for inconsistencies. Your language tries to be declarative throughout, but it turns out that it can't express any action without procedures. > If this is what you meant, then I guess I would say that > my system is either both "imperative" and "declarative" or > neither one; procedures will play an important role in the system, > and statements will also (when I begin working on the logic > part of the system), but neither the "procedure" or "statement" > has extra-special status in the system. There are two primary components of your system: functions, which are declarative; and procedures, which are imperative. I'll mention that in the next paragraph. > Actually, it has been a thought in my head that the distinction > between "statements" and "commands" is somewhat artificial... > Commands can be re-expressed as statements, and vice versa. This distinction you're making seems to be valid, but it seems to me to contradict everything you've said in the past about your system versus mine. For example, you state that your language can actually represent the number 3, while my system can only imitate 3 by means of a procedure. "Actual representation" is the goal of a declarative language; in such a language you want to make it seem like the author of a program is merely expressing immortal and immutable truths to the computer to help it understand the problem (the problem is also treated as an immortal truth). To help carry out this illusion, declarative languages usually do consistency checks to make sure that the author doesn't attempt to "contradict" himself, and usually disallow redeclaration or changing of data. > Similarly, the statement, > "Two plus two equals four." > could be expressed as a command as > "Note that two plus two equals four." Right. Since computers only understand commands, this ability to translate from declaration to command is what makes declarative languages possible. However, not every command can be expressed as a valid declaration, so a declarative language has to check the declarations its user makes to ensure that he's not causing a (trivial) contradiction. Here's an imperative sequence: x = 1 x = 2 This is a contradiction to a declarative language. To make this valid, you have to use lexical scopes: x = 1 in: x = 2 Now the inner x is different from the outer x. The problem is that objects are, almost by definition, mutable. An object, once declared, can change. Consider the dishwashing example we gave: x\wash x.rinse x.dry x In that example, lexically x is constant, but each step of the process changes the thing x holds, thus making the original declaration false. This is why I argued with your implementation, preferring instead my own: x\dry (rinse (wash x)) or @dry @rinse @wash This is still clearly declarative, but now we're not pretending to keep a constant 'x'. We instead have a theoretically different object which is returned from each function. Now, as an implementation detail, most languages actually keep the same object and simply modify it; but that's merely a convenient optimization. (Note that that optimization would be wrong for a declarative language if the function used 'x' somewhere else in the same lexical scope.) Since Joy makes all parameters implicit, it's impossible to express this type of contradication -- so even the ugliest, most imperative Joy code is closer to declarative code than all but the cleanest lambda expressions. Your language, thanks to its support for combinators, shares this advantage with Joy. The only difference -- not a bad one -- is that in your language parameters are explicitly tied to their functions. > I have indeed gotten away without having special parsing support for > combinators (although this doesn't seem like an amazing feat > to me). Here is an excerpt from the parser of my system: This was what I needed. Thanks! > [sudden realization that I could have just used strchr()]... > This is a messy way of reading the word; words are terminated > by "." in the system. Right. You also used three-character abbreviations for global variables. Double icky. You do know that one of the regrets of the inventors of Unix is "I wish we'd spelled CREAT as CREATE." What suffering are you saving yourself by naming a variable "siz" instead of "size"? And why are you calling it "size", when it has nothing to do with a size? > Anyway, my point is that the parser > does not specially mention any combinators; combinators are > expressed using just ordinary words. Of course, certain > combinators are mentioned specially in other parts of the system. A point well-made. > > remember that). Your system will require: > > > > - atomic functions and types (assuming that you don't go > > "whole horse" and > > just define everything in terms of the basic combinators, > > which is certainly > > possible but VERY expensive) > types... types are interesting things that I've been thinking about > lately... my current system is rather untyped. Not quite -- your current system relies on atomic types. You don't have type checking on your functions, but you DO use atomic types. I can tell because you're using 32-bit ints to represent numbers. If you had been using just elementary combinators to represent numbers, THEN you wouldn't need atomic types. > The main ability > that one would associate with "typed systems", I suppose, is the > ability to arbitrarily construct restricted functions, i.e. > functions that only meaningful when applied to a certain class > of parameters. Type-restriction is orthogonal to atomic types. > > > 1) In a purely-applicative system of the kind I've described, > > > it is always possible to replace a sub-expression with > > > one of equivalent meaning, while this is not the case > > > in the Joy system, because of Joy's heavy dependence on > > > a quotation construct. To illustrate, suppose in Joy one > > > had a program > > Not true -- as you admit below, the quotation construct has > > little to do > > with it. The problem is caused by being able to treat > > quotations as lists. > > If that were not possible, or if it were restricted in > > certain ways, there > > would be no problem even in this circumstance. > Right, it looks like you're not denying that there is this problem > with Joy... If you put it that way, I do deny that. I am claiming that there's a problem with certain aspects of code self-modification, and I'm claiming that this is a problem inherent to self-modification. > you're just denying that it stems from the quotation > construct... the solution you propose (restricting the ability > to treat quotations as lists) is one that the author of Joy has > considered; it has the disadvantage that you would then need > to have separate constructs for lists (if you still want them). > But, as far as I can see, it does look like a remedy that could work. Certainly. I don't think it's a good idea, though. A better idea would be to define any function which breaks under that type of treatment as "broken". For example, suppose we have a function which takes as input a number and a quotation; the quotation is supposed to represent an augmentation of that number. It produces as output a function/quotation which returns the augmented number. Some examples: 2 [2 +] augment i == 4; 2 [3 *] augment i == 6; First, I'll define a "broken" function which handles this. broken_augment == uncons cons cons; The error here is obvious: it assumes that there are two components to the augmentation function, a number and a function. This breaks down in the following case: 2 [0 +] broken_augment == ???; A correct augment function is: augment == cons; The fundamental error in the broken definition is that it fails to deal with the end-case, when the length of the quotation is zero. (It's merely coincidence that the repair is so trivial; in general, dealing with endcases is harder.) I claim that any function which fails to deal with the endcases will be broken; every function which does deal with the endcases (as well as the normal cases) will not be broken (in this sense), and will therefore handle optimization within its parameters correctly. > > > foo (plus 0) > > > could be freely rewritten as > > > foo I > > > regardless of the structure of "foo", if the system had > > > realized that the expression "plus 0" means the same > > > thing as "I". > > And the exactly analogous expression "0 plus foo" could be > > freely rewritten > > as "foo" in Joy. > Let me explain why I think this way... Suppose instead that > we have "WB (plus 1)"... "WB" is a higher-order function that > takes a function, in this case "plus 1" (the add-one function), > and yields the function's composition of itself; in this case, > that will be "plus 2". > Now, could you see that a Joy analogue of "WB (plus 1)" would > be "[1 plus] dotwice", where "dotwice" is a combinator that > takes a quoted program and does that program twice > (such a "dotwice" could be formed as "dup [i] dip i", I think). That would be bad -- try instead "dup concat", since you want to return a function. > In contrast, I cannot think of any "foo" such that "WB (plus 1)" > would be analagous to "1 plus foo" of Joy. You're right. > > Entirely correct. It _does_ cause an impediment; the > > interesting thing is > > that it itself (treating programs as lists) is a form of > > automated reasoning > > about programs. This suggests that the problem it causes _might_ be > > fundamental, one of the classic Godel incompleteness > > problems (as phrased > > for concatenative languages, of course). In other words, > > reasoning about > > programs may be easy; but reasoning about programs which > > are themselves > > rasoning about programs is not easy. > This is an interesting thought... But, I'm not sure that this > (the problem of not being able to substitute equivalent expressions > within quotations) is really a fundamental problem; my purely > applicative system does not seem to have it, and you've given > a remedy that could possibly rid a Joy-style system of it. Fundamentally wrong. Read my paragraph again: I'm not claiming that Joy has a fundamental problem; I'm actually claiming that _reasoning about programs is fundamentally difficult_. Your applicative system does not manifest this difficulty because _it is impossible to use your system to reason about programs_. As soon as you add reasoning capability your program will have to face the exact same problem -- and the same solution I outlined will work for you, and any solution you come up with will also work for me. > > > 2) Many fundamental equivalences of Joy programs that one > > > might like to show require restrictions, while > > > similar equivalences of purely-applicative expressions > > > can be taken unrestrained. Specifically, one might > > > like to think upon seeing a Joy program of the form > > This is _really_ heavily unfair. Those things aren't "fundamental > > equivalences"; they're rewriting rules. Rewriting rules of > > that nature are > > a thing used by syntax-based languages, and you honestly > > can't expect them > > to apply to a non-syntax based one. The fact that some of > > them do anyhow is > > almost amazing. > Hmm... I'm not sure what you mean by a "non-syntax based" langauge. > All languages (including Joy) have a syntax, do they not? Most Forthers would disagree with you. Quite simply, no parsing done by the interpreter in Forth can ever affect the interpretation of anything outside of the current word. Immediate words, of course, add another complication to this; but this makes _those_words_ syntax-based, not the language itself. > From this comment, I guess I can see that you just do not > value too highly the ability to do "textual reasoning" in > a language (i.e., reasoning which is achieved by changing expressions > around mechanically, but without paying special attention > to their meaning). > Yet, textual reasoning (or "formal reasoning" as it is usually > called) is something that I find quite interesting; it seems to me > to be a very good quality of a language if it can be easily > textually-reasoned upon. Formal reasoning is VERY powerful, and I used to imagine that it was difficult with Forth. At the same time, though, I found Forth very easy to work with; I never realized until I read the discussion of Joy that I had been applying a different type of formal reasoning to my Forth programs. Joy, for me, is not so much a language as it is an explanation of a system of formal reasoning which works for Forth. THIS is why I like Joy so much -- it takes an existing language, one which has already proven its worth (and its limitations), and it explains why it's so good -- and what can be done to fix it. Your language, OTOH, takes existing theory -- which is of dubious use, and is only around because it loosely models existing programming languages -- and attempts to build a programming language on it. Of course, if I were to say that Joy was any better than your language I'd be wrong. It's also a wild speculation, and odds are just about certain that it'll never be of any real use. The real value I'm looking at is the possibility of building a child of Forth which is just as useful as Forth, and just as easy to formally reason about as Joy. > (this holds for all well-formed Joy expressions "x", "y", > "a", and "b", > and by "x is replacable by y" I mean "x can be textually replaced by > y in any program and the resulting program will make the system do > the same thing as before"). Yes. Much better ;-). > > > The analagous equivalence in a purely-applicative system > > > is that > > > @@@C f x y > > > can be rewritten as > > > @@f y x > > > This can be accepted without restraint for all expressions > > > "f", "x", and "y" of the system. > > That's because combinators in your system have syntactic > > effect. In order to do that, I'd have to define: > > C == dip [swap] i; > > [y] [x] [f] C == [x] [y] f > Hmm... I suppose you meant "C == [swap] dip i"... Wow. ;-) Yes. > but, yes I suppose you could then accept "[y] [x] [f] C == [x] [y] f" > unrestrained. But, I don't think this principle is analagous > to the "C f x y = f y x", because the former would almost never > be used at all in a Joy system at all, while the latter would be > very commonly used in my system. Correct. It's nevertheless exactly analogous, under the same reasoning you so aptly used to demonstrate that [1 plus] foo was the Joy equivalent of your foo (plus 1). In both cases, Joy would rarely use the expression, while your language would use it constantly -- not a bad feature of either. > On the other hand, a reasoning > system in Joy would probably commonly find itself reasoning > that "x y swap" is the same as "y x", but it could only realize > this once it verified that "x" and "y" were simple programs that > just pushed a thing onto the stack with no other effect; > this might not be such an awful problem, since a reasoning system > would probably want for other purposes information on how many > items a program might push onto the stack. Your error here is assuming that the meaning of "x y" is dependant on "swap". In a concatenative language it simply isn't: "x y" may be rewritten as "y x" if x and y are commutative under composition. "x y swap" may be rewritten as "y x" if x and y are commutatative under composition, except that the 2 TOS items after "x y" are reversed. And so on. The important thing is to realize that _function_composition_equals_function_concatenation_ is the only rule in the language. If you invent a rule which disregards that fact, as this one does, you're going to have to place lots of qualifications on it. If you only use reasoning which accepts this fact, your reasoning process will be clear and clean. > > I also suspect that even the strength of your functional language, > > functions, will be very hard to read and maintain without > > automated help. > > APL functions don't use many combinators, but APL > > programmers recommend > > deleting the program and rewriting it based on the comments > > rather than > > trying to understand the program. Forth programmers don't do that. > confession... in working in my CLIP system, on several occasions I've > found myself deleting lines and rewriting them after they didn't > work on the first try, rather than trying to understand them :-) > but, i think this reflects my own unfamiliarity with the system > and also the inconvenience of the system's current syntax. APL experience indicates that it may be a fundamental fact rather than a temporary inconvenience. My suggestion is to design your language to make that type of thing even easier than it is now. That way you can borrow the slogan used by at least one APLer: "APL: if you can say it, it's done." APL, in my limited experience, is a real pleasure to work with. > - iepos ("Brent Kerby") -Billy From tcn@tunes.org Mon, 28 Feb 2000 18:04:27 -0500 (EST) Date: Mon, 28 Feb 2000 18:04:27 -0500 (EST) From: Tom Novelli tcn@tunes.org Subject: Languages. On Fri, 25 Feb 2000, Tril wrote: > On Fri, 25 Feb 2000, Alan Grimes wrote: > > > I think Tunes should be written in Assembly (or forth). > > As we figure out what we're doing we can start abstracting it untill we get a > > full-fledged HLL, but in the mean time I vote for ASM or FORTH. > > Shouldn't we know what we are programming before choosing a language and > starting to write code? Also, why does a whole project have to be in one > language? It's fine to argue for ASM or Forth, but what aspect of TUNES > design is suitable for implementation in them? Assembly and Forth? I've been working on that for a couple years now. My project (Retro) provides basic system services, a monitor program, and some programming utilities. It can easily be extended to handle LISP and other languages. Still, it's not Tunes... there's no fancy compiler, persistent storage, etc. You could use Retro to experiment with some of these things - decide what's really useful, polish and simplify the techniques. Tom Novelli