35 comments

[ 1484 ms ] story [ 4002 ms ] thread
Ah, thanks for posting about my Fexl language. Although I'm the author of it, and somewhat fond of it, I must emphasize this caveat: http://news.ycombinator.com/item?id=2717560 .

Although lazy evaluation is quite amazing, in certain circumstances I find that it's kicking my ass. For example, consider a function that simply sums the numbers from 1 to N:

  \sum == (\N
      long_le N 0
          0
          (long_add N (sum (long_sub N 1)))
  )
Or, more tersely, using the semicolon as a syntactic "pivot" to avoid right-nesting:

    \sum == (\N long_le N 0 0; long_add N; sum; long_sub N 1)
The problem is that when you call (sum 100000) it builds up a giant chain of (long_add 100000; long_add 99999; long_add 99998; ...) and then evaluates that monstrosity recursively.

So I need to do some more work on forcing early evaluation. You can start by using the standard accumulator trick:

  \sum == (\total\N
    long_le N 0
        total
        (sum (long_add total N) (long_sub N 1))
    )

  \sum = (sum 0)
But even then you still have the massive recursion problem because you're not forcing the addition operation early.

I'm thinking I just need to allow basic types like "long" to be called as functions with no effect (i.e. equivalent to the identity function, so you can do things like this:

  \sum == (\total\N
    long_le N 0
        total
        (
        \total = (long_add total N)
        total;    # force evaluation; result is I (identity function)
        sum total (long_sub N 1)
        )
    )
However, even that doesn't always do the trick, particularly when you're dealing with higher-level values that aren't basic data types. The problem occurs generally when you build up a big "chain" of calculations with arbitrary values, and you have no simple way of forcing evaluation along the way.

This is, of course, the classic struggle between eager and lazy evaluation. But if I don't do the evaluation lazily, I can't define recursion in terms of the closed-form Y combinator, namely (Y F) = (F (Y F)). Instead I'd have to define it in terms of some kind of run-time "environment" using either key-value pairs or the de Bruijn positional technique -- something I've managed to avoid thanks to lazy evaluation in terms of pure combinators.

So I must say that although Fexl is an interesting pure-combinator lazy evaluation language, the jury is still out on its practical utility, in my humble opinion. I've used it in some projects as an embedded interpreter, but the application was quite constricted so you didn't encounter some of these larger issues.

That is why I emphasized this caveat earlier today: http://news.ycombinator.com/item?id=2717560 .

Have you looked at how people deal with too much lazyness in Haskell? If yes, is it applicable to your language? Why, why not?

In your example I'd use foldl' (the prime is important) in Haskell for a strict sum.

Thanks for the tip -- I do see something in Haskell about marking things as strict. Perhaps I can do something similar in Fexl. The general problem I'm dealing with is long chains of state transformations like this:

  \chain = (\state
    \state = (event1 state)
    \state = (event2 state)
    \state = (event3 state)
    state)
That of course is simply equivalent to:

  \chain = (\state 
    event3; 
    event2; 
    event1; 
    state)
But the former is a way of showing the computation in a forward instead of reverse direction. And I know I could rephrase the former in a monadic style, but that in itself does not alleviate the problem of the lazy evaluation.

This certainly does not matter if you're just chaining three events together, but try linking that chain together 20000 times to give you 60000 events. Oh it works, but it's nasty with memory usage.

So maybe I can introduce something into Fexl, without sacrificing elegance, which forces some level of evaluation of the event applications.

I did try forcing at least a top-level evaluation of each event along the way, using a technique sort of like this:

  \eval = (\state state I \_\_ I)
(That's because I know the state is ultimately just a list. I have a really efficient way of doing arbitrarily large key-value maps simply using nested lists in just a few lines of Fexl.)

Then I did this bit of nastiness:

  \chain = (\state
      \state = (event1 state) eval state;
      \state = (event2 state) eval state;
      \state = (event3 state) eval state;
      state)
But I dunno, it still didn't quite do the trick. The jury's still out. So far for most "real work" I'm still just using embedded simple token-based domain-specific concatenative languages, with the enclosing interpreter written in either ANSI C or Perl. Fexl is still mostly a lab toy.
You can control the order of execution of pure functions by using CPS (so strict can be represented by lazy or vice versa). You can't force monadic operations to occur out of order this way: you need to have some concurrency between the pure expansion semantics and the action semantics.

Conal Elliot has written some nice things in this vein; he makes a relevant point in http://conal.net/blog/posts/can-functional-programming-be-li...

So why can't you interleave the add operations? Are the atomic arithmetic operations side effects? Can you not represent CPS faithfully for some reason? I'd really like to see the expansion phase of Fexl expressed using CPS.

BTW, borrow a notation from Haskell and have a dot operator be the transpose of the semicolon operator.

(Intriguing suggestion about the dot operator by the way.)

On this question: "Are the atomic arithmetic operations side effects?" Not really. Well, sort of. I mean, take a look at the reduction code for adding two long values: https://github.com/chkoreff/Fexl/blob/master/src/long_add.c

In short, when you evaluate (long_add 2 3), that value is replaced with the number 5, right inside the machine data structure. So in that sense there is a "side effect", but it's a purely functional referentially transparent side effect only in the C internals -- nothing mutable going on at the Fexl level.

I'm all well-versed with CPS (continuation-passing style), e.g. I've done stuff like this:

  \do_stuff = (\state\return
      do_this state \state
      do_that state \state
      return state)
But that doesn't in itself help me, yet.

By swapping the order of the parameters "state" and "return" in do_stuff, do_this, and do_that, I can transform that function into a monadic style:

  \do_stuff = (\return
      do_this;
      do_that;
      return)
But as it turns out that accomplishes nothing essential -- it is merely a syntactic difference.

Keep in mind that Fexl is purely combinatorial, and ultimately what's really going on under the hood are the application of these two rules:

  C x y    =  x
  S x y z  =  x z; y z
So maybe that will give you some insight into just how irredeemably lazy this language really is. :)

(Yes there are some other combinators such as I, L, R, and Y, but these are ultimately shorthands for S and C forms.)

If by "interleave the add operations" you are suggesting a change to the core evaluation strategy used in the interpreter, that is probably out of the question -- I've made my bed there and I have to lie in it. There's not much I can do at this point about my reliance on combinators, I mean, check out the S combinator: https://github.com/chkoreff/Fexl/blob/master/src/S.c . That's baked in the cake!

But if you mean there's something I can do different in my Fexl function itself, that might be something to consider.

I tried the full gamut here, using both accumulator and CPS:

  \test_big_sum_4 =
  (

  \sum == (\N \total \return
      long_le N 0
          (return total)
          (sum (long_sub N 1) (long_add total N) return)
          )

  # TODO still a problem!!
  \N = 100000
  sum N 0 \total
  print "sum 1 .. "; print N; print " is "; print total;nl;
  )

  test_big_sum_4
But to no avail: it still uses up large amounts of memory.

However, I could force the evaluation of (long_sub N 1) and (long_add total N), and that might do the trick. Then it'll be totally tail recursive with machine integers at every turn, and run in constant memory.

I'm all well-versed with CPS

I'm talking about a particular application of CPS, the encoding of CBV lambda-calculus in the CBN calculus. Checkout Danvy & Filinksi (1992) if you need brushing up on this: look at what happens in your calculus when you code up the CBV version of the foldl, which should force the first atomic operation to happen before unwinding the next application of addition.

Danvy & Filinksi, 1992, Representing control: a study of the CPS transformation http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.8...

In general, I've found explicit lazy evaluation much easier to deal with, such as Python or Clojure. It is potentially less powerful, however.

Btw, interesting language, this Flex of yours.

Thanks -- small typo though, it's actually "Fexl" (pronounced sort of like "pixel").
the discussion of side effects on that page is confusing. all you seem to be saying is that any function can be redefined. you do not need to mention side-effects to say that. it is confusing because anyone reading who knows about functional programming is going to see mention of "side effects" and "print" and expect some kind of discussion related to the problems that monads in haskell address (that you can change and examine state in the file system).

also, you should explicitly say somewhere that it is eager (not lazy) (if it is).

also, i couldn't find any discussion of whether it is possible to mutate state (i assume not, but you don't say). related, a table of contents would be a big help - and obvious initial question is "how are data structures handled?" and you don't know where to find the answer when you at the top of the page.

(this is not meant to say that your language is bad - i am just trying to help you "sell" you language to people that read your page!)

Yes, I do need to update things there. I mentioned redefinition in the context of side-effects because I wanted to emphasize that you could isolate a Fexl function in a safe "sandbox" so you could prevent users from calling "print" or "unlink" directly, or even substitute a simulated file system so they can call "unlink" safely. But you're right, they are two separate issues.

On your second point, Fexl is definitely not eager. It is the laziest thing you'll ever see.

And no, you cannot mutate state in any way.

On the subject of "how are data structures handled", I do address that at the top in "RULE 1: Everything is a function." There I say that all data are represented as functions, and I do show a little link to some exposition below.

Thanks for the help on "selling" the language -- until now, I haven't been concerned about that because it's all for my own purposes. But I take your point.

(comment deleted)
What is an "expression language"? I just started doing Java EE crap and there is an assortment of "ELs" that you can embed in your java apps.

Outside of Java, an "expression language" doesn't make any sense to me. An expression, statement, binding, assignment, application, and abstraction are all constructs, or elements of programming languages. Most of them can be implemented using others, sure, but I expect all useful languages to be capable of all. So, my question is, what makes an expression language an expression language, to the exclusion of all other constructs? (IOW, why is that particular part being made into a defining characteristic of the language?)

:) It's a function expression language, meaning a language for expressing pure functions of an arbitrary nature. It's really just a variant of the lambda calculus, and I compile those expressions into combinators to eliminate all variables.

For example, the "flip" function for swapping the order of two functions is this:

  \flip = (\x\y y x)
But Fexl converts that into pure combinators like so:

  \flip = (S (C (S I)) C)
Actually it uses the higher level combinators L, R, I, etc. so that flip is defined as:

  \flip = (L I)
But the higher-level are shorthand for forms that use S and C only.
I hear ya. I consider Fexl to be at the extreme end of computational abstraction. The kind of things you reference are convenient tools for making certain lower-level things accessible from a high level sort of script.

I've used that approach in my work for many years with great success. I use ultra-simple domain-specific languages consisting of this syntax:

  token token token token ...
:) Seriously though. It's amazing what you can do in a script consists of nothing but tokens or "words", e.g.

  do_this 4 5
  do_that x "Hello there."
Where all filler such as #-comments and white space, even line breaks, are completely insignificant. I've made a lot of hay out of languages like that.

I even have a Turing-complete language in that form, and I can define "verbs" which access any sorts of machine functions I care to provide. It ends up looking a bit like Forth but I think more clear (coming from a biased judge of course). I'm actually tending to favor that token-based Turing-complete language over Fexl for doing real work. I'm not devoted to the idea of functional programming, I'm devoted to simple, powerful, flexible, and secure programming.

Strangely, the token-based languages make my application code more "language independent" -- meaning that I can write the interpreter in any language I want (C, Perl, etc.) and it really does not matter at all. I like to keep pushing as much application logic into the scripting language as possible. With the Turing-complete language it might even be feasible to do all of my application logic in it, leaving me with only a small residual interpreter written in C. But it remains to be seen if my Turing-complete token language can really scale well to that large body of application logic.

So, how is this different/better than other functional languages?
OK, I'll discuss what's different and leave "better" out of it.

1. In Fexl, there is no distinction between data and function. All data structures such as lists, pairs, etc. are represented as functions.

2. Fexl has a small grammar, about as small as I think is feasible for expressing arbitrary functions. You could actually omit these rules:

  exp => \ sym = term exp
  exp => \ sym == term exp
  exp => ; exp
But the resulting forms would be far more difficult to write and understand.

3. Fexl has a very simple compilation and evaluation strategy, reducing everything to combinatorial forms. There are no "environments" or "closures" or "contexts" being whipped around at run-time. The resulting Fexl executable program is about 35K in size.

4. Unlike other functional programming languages, Fexl does not rely on "pattern matching" for branching on the different possible forms of a piece of data. Instead, it simply calls that piece of data as a function, passing in the appropriate handlers for the various cases. For example, using excessively verbose function names here:

  list
      handle_empty_list
      \head\tail handle_first_item head; handle_remainder tail
5. Fexl doesn't really have a distinct concept for defining a name -- that's all handled normally by lambda calculus. However it does provide the syntactic shorthand "=". For example this function:

  \square = (\x mul x x)
  print (square 4)
Is equivalent to this function, which does not use the "=":

  (\square print (square 4)) (\x mul x x)
I'm not exactly sure if that's oh-so-different from other programming languages, but I'll venture a guess that many of those other languages use a symbol table to store function definitions at run-time, while Fexl does not.

6. Fexl never creates circular data structures in memory. Now because of lazy evaluation, you can create logically circular or infinite structures in Fexl, but these are always closed forms and never involve literal circularity in memory. Consequently it is possible to manage memory using reference counting -- and Fexl does that. Some may not like that, but it's simple and it gets the job done. The code even has a built-in assertion to ensure that all memory was properly reclaimed at the end of a run.

(I'm not certain that other languages create circular structures in memory, but I am certain that Fexl does not so I thought it worth mentioning.)

That's interesting, but excluding the "reducing to combinatorial forms" thing, which only seems to be an implementation detail, can't this all be done in Scheme or another Lisp dialect? "no distinction between data and function" goes hand in hand with https://secure.wikimedia.org/wikipedia/en/wiki/Homoiconicity
You can certainly do this sort of thing in Lisp, using forms like:

  (defun square (x) ...)
  (lambda (x y) ...)
  (add 2 (add 3 (add 4 5)))
In Fexl you would see instead:

  \square = (\x ...)
  \x\y ...
  add 2; add 3; add 4 5
So at least Fexl has the virtue of being more compact in those cases. :) Also, in Fexl, whenever you see a name, it always refers to a function, unlike in Lisp, where names like "defun" and "lambda" and "prog" are meta-logical syntactic devices and cannot be defined as functions in their own right. And anything like "setq" or "setf" is strictly out of the question in Fexl.

You could of course implement lazy techniques in Lisp, even going so far as to write a Fexl interpreter if you like. I just wanted to see what happened if procedural, mutable, and meta-logical constructs were completely eliminated as possibilities in a language.

So, Haskell without types and monads basically? ;)
Definitely without the types, yes. However, monads are completely do-able in Fexl. I can write Fexl code that looks procedural and "side-effect-y", using the monadic technique so that you never actually see the state variable that's being chained through the functions. Monads are more a style of code than a feature of the language per se.

Personally I'm quite happy without the baggage of type declarations. As long as I build up functions systematically, I have very few problems with run-type type violations. Sure every once in a while I forget a semicolon or whatever and my function gets "out of synch" like a T-1 line gone out of phase. But it's usually pretty easy to see what went wrong.

Yeah, I know how monads work, what I mean was that according to the examples you don't seem to enforce purity(nothing like the IO monad).

And I've never found types to be a burden in Haskell, the type inference works great so that you can usually omit them if you can't be bothered, but I still include them most of the time because they help you reason about your code and see patterns.

Right, I don't enforce purity, but I do allow it. Ultimately there's a "string_put" function which (1) produces an actual side effect and (2) evaluates to the identity function. You can wrap monadic (monastic?) purity around that if you like.

Also, strict typing is pretty much impossible in Fexl, since I'm using combinators. You can't really assign a meaningful type to things like S, C, I, Y, etc. So yes, Fexl is very "loosey-goosey" that way. I also didn't want to bother with some ponderous PhD project like a "type inference engine" written into my ANSI-C interpreter. I figure if you want to do high level things like that, write those tools in Fexl itself (i.e. use meta-programming techniques).

You can change R3RS Scheme a little to get a language whose types are all functions that defines cons in the same way as in the lambda calculus, but passing around symbols to describe types:

     (define cons (lambda (car cdr) (lambda (z) (z 'pair car cdr))))
     (define car (lambda (pair) (pair (lambda (type car cdr) car)))) ; we don't actually check type correctness in this simple example
     (define cdr (lambda (pair) (pair (lambda (type car cdr) cdr))))
     (define type-of (lambda (x) (x (lambda (type . rest) type))))
     (define pair? (lambda (x) (eqv? 'pair (type-of x))))
Similar techniques can be used to code Booleans, nil, &c.

Symbols in turn can be lifted out to be functions, whose type "symbol" is some particular function, say identity: here we need to have an atomic operation to define whether two functions are realised using the same closure, which is the usual meaning of eqv?.

While Scheme has mutable structures (e.g., set-car!) and circular structures, this alternate language would not.

Note that the Haskell 98 standard does not require implementations to let circular structures be defined, ghc does allow them (e.g., ones = 1:ones is a circular structure in ghc, but Haskell 98 would allow a Haskell to implement it as an infinite list).

i dont understand the identity function that uses just C and S. :(

C x y = x

S x y z = (x z) (y z)

\I = (S C C)

so uh... that would evaluate to (\z (C z) (C z)), right? and then to (\z z z)? but then you got z twice. what does that even mean? applying z to z?

I z = (S C C) z = (C z) (C z) = C z _ = z

Where _ stands for a value that doesn't matter.

And actually for any x we have S C x == I.

Yes, I like that expository technique of using "_".

Often when I develop a Fexl function I actually use "_" as a placeholder for "something I haven't implemented yet". So if I'm doing something with a list, I might say:

  list
    _
    \head\tail _
And then I just go back and replace each "_" with an implementation of that particular case. In doing so, I might create new "_" slots ("holes"), which I then implement, and then I repeat this process until no more holes appear. It's a really good "case analysis" approach to programming.

For example I used this approach to implement an associative key-value map structure which uses nested lists, where a list at level N branches on the Nth character of the key, so it's a radix-style algorithm but without splitting the keys into pieces:

    # Helper function.

    \map_put == (\map\pos\key\val
        map             
            (item (pair key (pair val end)) map)
            \head\tail
            head \top_key\top_node

            # Do a three-way comparison of key char at pos
            # with the top key char at that pos.

            long_compare (string_at key pos) (string_at top_key pos)

                # key char is less than top key char
                (item (pair key (pair val end)) map)

                # key char equals top key char
                (
                top_node \top_val\top_map
                \new_head =
                    (
                    string_compare key top_key
                        (pair key (pair val
                            (map_put top_map (long_add pos 1) top_key top_val)))
                        (pair key (pair val top_map))
                        (pair top_key (pair top_val
                            (map_put top_map (long_add pos 1) key val)))
                    )
                item new_head tail
                )

                # key char is greater than top key char
                (item head (map_put tail pos key val))
        )   
    
    # The actual function (map_put key val).  This just
    # calls the helper function with position 0 to start.
    # I should probably re-order the helper function so
    # pos is the last argument, then I can define this
    # function simply as \map_put = (map_put 0).

    \map_put = (\map\key\val map_put map 0 key val)
Thanks!

By the way, if you like SKI calculus, you would have loved this year's ICFP programming contest.

Oh I love questions like these, thanks!

You went off the rails because you replaced (C z) with z. That is not a valid equivalence. (C z) does not equal z. But (C z) applied to anything else does equal z. So in particular, (C z) (C z) equals z.

Here's how the reduction sequence goes:

  : S C C z
  = (C z) (C z)
  = z
If you think about it, that second C can be replace with anything and it still works:

  : S C anything z
  = (C z) (anything z)
  = z
That C there simply "holds on" to its first argument, and then when it encounters any second argument, it ignores it completely and yields up the thing it was holding onto.
Also, you'd be surprised at how self-application can actually mean something. Consider how a recursive function is defined using the Y combinator, which is governed by this rule:

  Y F = F (Y F)
OK now that's not exactly self-application yet, but now let's dig into the Y combinator a bit and see how it can be defined in terms of a Q combinator:

  Y = S Q Q
Now what is Q you ask? It can be defined as a lambda expression this way:

  \Q = (\x\y x (y y))
Aha! There's a self-application for you. See how y is applied to itself there.

Now let's use the abstraction algorithm to convert Q into combinators step by step, removing the variables:

  \Q = (\x\y x (y y))
  \Q = (\x S (\y x) (\y y y))
  \Q = (\x S (C x) (S (\y y) (\y y)))
  \Q = (\x S (C x) (S I I))
  \Q = (S (\x S (C x)) (\x S I I))
  \Q = (S (S (\x S) (\x C x)) (C (S I I))
  \Q = (S (S (C S) C)) (C (S I I))
OK that was kind of fun but all I did was demonstrate that Q can be defined in terms of S and C. That's actually somewhat irrelevant. The main point is to demonstrate that when you apply the Y combinator to an arbitrary function, you can in effect get recursion. So let's follow the reduction sequence:

  : Y F
  = S Q Q F
  = (Q F) (Q F)
  = Q F (Q F)
  = F ((Q F) (Q F))
  = F (S Q Q F)
  = F (Y F)
Now let's show how you can actually remove self-reference from a recursive function. Consider this recursive definition of the function which appends two lists:

  \append == (\x\y x y \h\t cons h; append t y)
Now as it turns out, the "==" in Fexl is merely a short-hand which does this for you:

  \append = (Y \append \x\y x y \h\t cons h; append t y)
That's actually a non-recursive definition of append there. Consider the body of the function by itself:

  (Y \append \x\y x y \h\t cons h; append t y)
There are no free variables in that. The inner call to "append" actually refers to the \append when encloses it. So you actually have a fully closed form function there which calls itself.

You can look at it this way, separating that inner function out and calling it F:

  \F = (\append \x\y x y \h\t cons h; append t y)
  \append = (Y F)
So if we actually apply that function to two lists, it goes like this:

  : append x y
  = Y F x y
  = F (Y F) x y
  = F append x y
  = (\x\y x y \h\t cons h; append t y) x y
  = x y \h\t cons h; append t y
And then it proceeds from there depending on what x is. It's amazing stuff, being able to defined a fully closed-form function which somehow manages to reach out and call itself. :)
Now on another topic which I find interesting, consider how the abstraction algorithm (conversion to combinators) can be simplified with the use of two helper functions L and R:

  \L = (\x\y\z (x z) y)  # send z to left side only
  \R = (\x\y\z x (y z))  # send z to right side only
Of course these can be defined in terms of S and C but I just implement them directly in the interpreter for efficiency.

Now let's abstract the Q definition using L and R in addition to S and C where possible:

  \Q = (\x\y x (y y))
  \Q = (\x R x (\y y y))
  \Q = (\x R x (S I I))
  \Q = (L (\x R x) (S I I))
  \Q = (L R (S I I))
That's much more compact than just using S and C alone.
Ah -- concatenative languages, yes. I'm becoming a big fan of simple token-based concatenative languages. See my summary here of my recent travails: http://fexl.com/second_thoughts .

In short, instead of Fexl I've started using a simple token-based language. Like Joy, my token-based language uses a stack. But my language also uses a global mutable key-value space, and it only supports keys and values which are strings (which may be interpreted as numbers).

I think my little language is easier to "execute" manually on a white-board in a way that's easy for non-programmers to follow along. I don't need a lot of arcane "stack flip-aroo" operators like Forth and Joy use.

So how do I do a sort you ask? I just jam a bunch of keys into the key-value space and then use "next" to iterate through them. The key-value space does the sorting for me.

What about strictly sequential lists you ask? I can just store a bunch of keys using a scheme like this:

  item/a1 ... item/a9
  item/b10 ... item/b99
  item/c100 ... item/c999
  item/d1000 ... item/d9999
By prefixing a single character which represents the number of digits, I get the proper numerical order when I iterate in ASCII character order.

You know what my real goal is? I want to be able to give my non-programmer colleagues the ability to embed little snippets of computation here and there, making simple things simple, but also imposing no upper bound on the possibilities of what they might do.

What a difference a day makes. The day after this discussion about my problems with lazy evaluation, I figured out an incredibly simple solution: http://fexl.com/eager_and_lazy