Lisk — Lisp and Haskell

  • While programming in Haskell I also struggled with Haskell's brand of whitespace indentation, even though I come from Python. The key difference in usability between Python's whitespace indentation system and Haskell's indentation system is that you cannot start the lines in a block on the same line as the block.

    Python's is more restrictive: you must skip a line before starting a multi-line indented block; i.e., this is not legal:

        if a == b: print "asdf"
            print "a"
        else:
            print "a"
    
    This lets you decide on a very simple rule for dealing with whitespace indentation: Each new block is started by inserting a carriage return and the proper number of tabs.

    Not so in Haskell, because you are given the freedom to put the first line of a block in the same line as the block starter:

        let x = 1
            y = 2
    
        do x <- 1
           y <- 2
    
    Now you need to decide whether to be 3 or 4 spaces in depending on whether it is a let or do block. You cannot use the rule, because the appropriate number of spaces is no longer a multiple of your indentation unit.

    That's just the simple case, because you are also allowed to put these block starters (let, do, case, etc) at arbitrary points in an expression:

        f = let x = 1 in let y = x + 2 in
         y + 1
    
    This is syntactically correct Haskell; but personally I like the idea of lexical scope being represented by indentation level, which is not reflected here.

    It becomes very easy to get yourself into situations where you cannot use the Python rule. But you can also impose restrictions on yourself so you _can_ use that rule. Like always treating let, do, in, etc as "{"'s in C:

        f = let
                x = 1
            in
                let
                    y = x + 2
                in
                    y + 1
    
    This may look a little verbose. But in real cases there would be a lot more statements there. In the middle of all this I want to maintain the idea "number of tabs corresponds to lexical scope". We can also push the analogy to "{"'s in C and adopt the "K&R" style, but on block-starting expressions:

        f = let
            x = 1 in let
                y = x + 2 in
                    y + 1
    
    
    There's also the solution of editors that just figure out where to indent, in which case we can make it look pretty and still get the indentation right. I think it's best to develop a consistent style that will work across editors though.

  • I don't see how it's tackling the 'where' block--or it that would even be possible under this scheme. I find that my code is much more legible with the housekeeping and other less important helper-functions defined after the important bits. The clunky code that the author complains about can be much improved using where clauses to break it into chunks.

    Taking the authors example and moving some of the processing to a where clause makes the flow much easier to follow:

        someFunction conn (Foo n) (K {x=zot}) plib = do
          withTransaction conn $ \db ->
             coconut <- sizzleQuery [Foob n]
             potato  <- sizzleQuery [Foob n]
             let sizzle = (zotify coconut) ++ potato ++ gravy
                 record = fasterize $ makeRecord' sizzle
                 date = dateOrError sizzle
              in catch handler $ insertIntoDB sizzle plib
    
          where sizzleQuery = queryTheDB "select * from sausages where sizzle = ?"
                zotify c = zot (plib $ zip [1..] c)
                dateOrError d = error "Unable to parse date"
                                `fromMaybe` parseDate d "date"
                handler e = do something `with` (k $ the exception)
                makeRecord' s = (MakeRecord { recName = sizzle "name"
                                                , recAge  = sizzle "age"
                                                , recDate = date
                                                })
                                    $ Plib </> (fromMaybe "" $ sausages >>= bacon)
    
    
    Also one of my favorite features of Haskell is using the $ as an unmatched left parenthesis, saving you from that blob of closing parenthesis that every lisp expression accumulates.

    One last thing, the author complains about the ambiguity of the indentation, but doesn't make any comment about the brace & semi-colon syntax. I personally don't like it, but it should be explained why it isn't an acceptable solution.

  • Here's my obligatory dumb question of the day. What is the problem he is trying to solve with the macros? It seems super straightforward, so I suspect I'm missing something (being only conversational in Lisp and Haskell, although don't know macros even decently):

    Here's the snippet of code that describes the problem (which I don't get):

    do exists <- doesFileExist "lalala" if exists then ... else ...

  • Not tackling pattern matching is a pretty big deficiency. Otherwise, it seems interesting, and I wonder if

      :i-o
    
    would really be easier to type than

      IO
    
    for most use cases.

  • How strange. I was just thinking about doing this as a toy project earlier today.

    I honestly don't share the OP's hatred of Haskell syntax, nor do I find it categorically worse than lispish languages. It's entertaining that he's gone from capital letters to the sigils (":"). Both tend to be controversial and which is better is more a matter of personal taste than anything.

    I agree with another commenter that the lack of pattern matching syntax is something of a deficiency, and I was also troubled over how I'd do this. A "plambda" operator is one possibility that I was considering, though it feels like it's a really hackey embedding of MLish in lispish:

         (plambda fname
                  ((pattern-1a pattern-1b ...) (stuff))
                  ((pattern-2a pattern-2b ...) (stuff)))
    
    Also, rather than a preprocessor I was thinking of implementing the whole thing in Template Haskell---not because it's easier, but really because I expect the opposite: I figured it'd be a good way to really bite off a big chunk of TH and get good at it.

    EDIT: It occurs to me that one could just use a variant on cond to implement pattern matching, e.g., "cond-argv":

         (define (fname a b) (cond-argv
                             ((a1 b1) (stuff))
                             ((a2 b2) (stuff))))

  • Everyone has their own syntactic preferences. And what, really, is the difference between a preference for certain colors in an IDE and a preference for a certain syntax? Why is one easily customizable and the other rigidly defined by a language? If programs in all languages were stored on file as lisp-style AST's, with comments attached to AST nodes, then programmers could view programs using their own preferred syntax, as well as formatting and colors.

  • Bit of a plug, I've written something like this except that it doesn't work via GHC and instead outputs source code (which means the gensyms aren't very safe...)

    I'm not working on it anymore (unfortunately never really got over the static typing) but it should run and might be of interest: https://github.com/aliclark/hasp http://www-student.cs.york.ac.uk/~anc505/code/hasp/hasp.html

  • Shouldn't:

    def fib(n):

        if   n < 2: return 1
    
        else      : return fib(n - 1) + fib(n - 2)
    
    Be: ...

        if   n < 2: return n
    
        ...
    ?

    Or am I being stupid?

    EDIT: changed x to n.

  • I remember something like this called Liskell, but I think it died.