37
votes

After a few months of learning about and playing with Lisp, both CL and a bit of Clojure, I'm still not seeing a compelling reason to write anything in it instead of C#.

I would really like some compelling reasons, or for someone to point out that I'm missing something really big.

The strengths of Lisp (per my research):

  • Compact, expressive notation - More so than C#, yes... but I seem to be able to express those ideas in C# too.
  • Implicit support for functional programming - C# with LINQ extension methods:
    • mapcar = .Select( lambda )
    • mapcan = .Select( lambda ).Aggregate( (a,b) => a.Union(b) )
    • car/first = .First()
    • cdr/rest = .Skip(1) .... etc.
  • Lambda and higher-order function support - C# has this, and the syntax is arguably simpler:
    • "(lambda (x) ( body ))" versus "x => ( body )"
    • "#(" with "%", "%1", "%2" is nice in Clojure
  • Method dispatch separated from the objects - C# has this through extension methods
  • Multimethod dispatch - C# does not have this natively, but I could implement it as a function call in a few hours
  • Code is Data (and Macros) - Maybe I haven't "gotten" macros, but I haven't seen a single example where the idea of a macro couldn't be implemented as a function; it doesn't change the "language", but I'm not sure that's a strength
  • DSLs - Can only do it through function composition... but it works
  • Untyped "exploratory" programming - for structs/classes, C#'s autoproperties and "object" work quite well, and you can easily escalate into stronger typing as you go along
  • Runs on non-Windows hardware - Yeah, so? Outside of college, I've only known one person who doesn't run Windows at home, or at least a VM of Windows on *nix/Mac. (Then again, maybe this is more important than I thought and I've just been brainwashed...)
  • The REPL for bottom-up design - Ok, I admit this is really really nice, and I miss it in C#.

Things I'm missing in Lisp (due to a mix of C#, .NET, Visual Studio, Resharper):

  • Namespaces. Even with static methods, I like to tie them to a "class" to categorize their context (Clojure seems to have this, CL doesn't seem to.)
  • Great compile and design-time support
    • the type system allows me to determine "correctness" of the datastructures I pass around
    • anything misspelled is underlined realtime; I don't have to wait until runtime to know
    • code improvements (such as using an FP approach instead of an imperative one) are autosuggested
  • GUI development tools: WinForms and WPF (I know Clojure has access to the Java GUI libraries, but they're entirely foreign to me.)
  • GUI Debugging tools: breakpoints, step-in, step-over, value inspectors (text, xml, custom), watches, debug-by-thread, conditional breakpoints, call-stack window with the ability to jump to the code at any level in the stack
    • (To be fair, my stint with Emacs+Slime seemed to provide some of this, but I'm partial to the VS GUI-driven approach)

I really like the hype surrounding Lisp and I gave it a chance.

But is there anything I can do in Lisp that I can't do as well in C#? It might be a bit more verbose in C#, but I also have autocomplete.

What am I missing? Why should I use Clojure/CL?

14
  • 4
    Since you're already programming in a functional style and it sounds like you rely fairly heavily on the Windows infrastructure, I can't see any compelling reason for you to change from C#. Most everyone I know uses Mac or Linux, so a lot clearly depends on the crowd you run with (I've used every version of Windows since 3.1 but I still prefer working with cross-platform software and *nix systems in general... but then I don't do any native GUI work, all servers and web UI stuff) Commented Feb 20, 2011 at 17:41
  • 2
    You could have a look at The Swine Before Perl. It's a fun talk to listen to, and presents a simple example of what macros are good for. Commented Feb 20, 2011 at 20:14
  • 4
    "I haven't seen a single example where the idea of a macro couldn't be implemented as a function" -- I'd like to see how you'd write AND or OR as a function. It could be done (given LAMBDA, which is also a macro) but I don't see an obvious way to do it that wouldn't totally suck.
    – Ken
    Commented Feb 21, 2011 at 17:01
  • 1
    "Namespaces. ... Clojure seems to have this, CL doesn't seem to" -- can you be more specific about what you're looking for? I've not really used Clojure but its namespaces look pretty much just like CL's packages.
    – Ken
    Commented Feb 21, 2011 at 17:05
  • 4
    Jonathan: CL uses : (or :: for un-exported symbols) for naming symbols in packages, e.g., your example here would use http:session and chat:session.
    – Ken
    Commented Feb 21, 2011 at 18:27

7 Answers 7

23
votes

Macros let you write compilers w/o leaving the comfort of your language. DSLs in languages like C# generally amount to a runtime interpreter. Depending on your domain, that might be problematic for performance reasons. Speed does matter, otherwise you'd be coding in an interpreted language and not C#.

Also, macros also let you consider human factors - what is the most user-friendly syntax for a particular DSL? With C# there's not much you can do about the syntax.

So Lisp allows you participate in language design w/o sacrificing a path towards efficiency. And while these issues might not matter to you, they are extremely important as they underlie the foundations of all useful production-oriented programming languages. In C# this foundational element is exposed to you in a very limited form at best.

The importance of macros is not lost on other languages - Template Haskell, camlp4 come to mind. But yet again it's an issue of usability - Lisp macros to date are likely the most user-friendly yet powerful implementation of compile-time transformation.

So in sum what people generally do w/ languages like C# is build DSILs (Domain Specific Interpreted Languages). Lisp gives you the opportunity to build something much more radical, DSPs (Domain Specific Paradigms). Racket (formerly PLT-Scheme) is particularly inspiring in this regard - they have a lazy language (Lazy Racket), a typed one (Typed Racket), Prolog, and Datalog all embedded idiomatically via macros. All these paradigms provide powerful solutions to large classes of problems - problems not best solved by imperative programming or even FP paradigms.

5
  • 3
    What I find interesting about that argument is that I've never run into an opportunity (or just completely overlooked it in my ignorance) for implementing a DSL/DSIL. Could you elaborate on the power of DSLs? (For both a developer and a user.) This sounds like it might be the big thing I'm really missing.
    – Jonathan Mitchem
    Commented Feb 20, 2011 at 18:15
  • 15
    @Jonathan Mitchem, you're already using a lot of external DSLs - configuration files, build scripts, ORM configurations, visual code generators (e.g., winforms editor), XAML, parser generators, etc. Embedded DSLs are even better, as you don't need a separate build tool for them, and they're served within a single language. And you must be familiar with at least two of the embedded DSLs - regular expressions and SQL.
    – SK-logic
    Commented Feb 21, 2011 at 18:09
  • Wow, ok. I just never thought of those as DSLs. Now, I've intentionally strayed away from a lot of that, on the basis of "anything I can do with c#, I'm going to stick with c# for". I like sticking with one tool with a consistent behavior, personally. But I do understand the value of DSLs in these examples.
    – Jonathan Mitchem
    Commented Feb 21, 2011 at 22:23
  • 2
    @Jonathan Mitchem, the problem with any given tool is that is won't necessarirly be fit for a given purpose. You have to choose different tools for different tasks. And the power of any meta-language, including Lisp, is that you don't have to fully switch to another tool, as you can grow your own domain-specific tools on top of your main language. I suggest that you take a look at Nemerle, it would be easier to understand it for a C# developer, and its macros are almost as powerful as Lisp's.
    – SK-logic
    Commented Feb 22, 2011 at 11:21
  • 3
    "I like sticking with one tool..." Properly crafted DSL's in Lisp never hide the full power of Lisp, so you still have one tool. They just add to the vocabulary in a way that makes coding for a particular domain more "natural", i.e. with better abstractions and overall organization.
    – semperos
    Commented Feb 22, 2011 at 17:28
15
votes

Almost everything that was originally available only in Lisps has thankfully migrated into a lot of other modern languages. The biggest exception is the "code is data" philosophy, and macros.

Code is Data (and Macros) - Maybe I haven't "gotten" macros, but I haven't seen a single example where the idea of a macro couldn't be implemented as a function

Yeah, macros are hard to grok. Metaprogramming in general is hard to grok. If you saw any macro that could be implemented as a function, then the programmer was doing it wrong. Macros should only be used when a function won't work.

The "hello world" example of a macro is to write "if" in terms of cond. If you're really interested to learn the power of macros, try defining a "my-if" in clojure, using functions and watch how it breaks. I don't mean to be mysterious, I've just never seen anyone start to understand macros by explanation alone, but this slideshow is a pretty good intro: http://www.slideshare.net/pcalcado/lisp-macros-in-20-minutes-featuring-clojure-presentation

I've had small projects where I saved literally hundreds/thousands of lines of code using macros (compared to what it would have taken in Java). Much of Clojure is implemented in Clojure, which is only possible because of the macro system.

25
  • 3
    Without touching any code, I ran through the scenario of implementing an "if" without macros in my head, both in Clojure and C#. It effectively (or literally) can't be done. I admit that's neat. But I'm missing the connection of how that's useful to me. What can I do with macros (besides write "if") that I can't do with clever functional or OO-design? "How could this help me" isn't a valid criteria for evaluating a language, but it is for evaluating whether I'll use it. (I really want a Lisp to be worth switching to, but it hasn't hit the point of becoming a viable alternative.)
    – Jonathan Mitchem
    Commented Feb 20, 2011 at 16:40
  • 1
    It's going to take me a bit of time to grok that example. For me, "saving boilerplate code" usually invokes the term "refactoring", which is part of why the explanations haven't convinced me. I have a solution that works in every instance I've applied it. I think then the question is how do you identify cases where you can use a macro?
    – Jonathan Mitchem
    Commented Feb 20, 2011 at 17:09
  • 1
    I agree that macros are worthwhile, but it isn't true that a my-if requires them, it can be written as a higher-order function (see a CL example below). You only really need macros if you want to walk the code or place it in a new lexical context. (defun my-if (test then &optional else) (cond (test (funcall then)) ('otherwise (funcall else))))
    – Rörd
    Commented Feb 21, 2011 at 0:33
  • 1
    Basically with a function you have a choice to accept either quoted code, which you can walk but it's missing its lexical context because you're walking and executing it in the lexical context of your function. Or you can accept closures, which keep their lexical context but you can only call, not walk them or add something to their lexical context. To do both you need a macro, which can walk and wrap the code before its expansion will be placed into the lexical context of the macro call.
    – Rörd
    Commented Feb 21, 2011 at 0:49
  • 7
    @Jonathan Mitchem, LINQ syntax is a nice example of what can be done with macros, but had to be implemented in a language core instead, as the language does not support any decent metaprogramming.
    – SK-logic
    Commented Feb 21, 2011 at 18:13
14
votes

I'm not an expert in Common Lisp, but I know both C# and Clojure reasonably well so hopefully this is a useful perspective.

  • Simple syntax => power - Lisps are about the simplest syntax that could possible work. S-expressions are all you need. Once you've grokked "(function-call arg1 arg2 arg3)" then you've basically got it. The rest is just picking the right function calls and arguments. Seems almost trivially simple, but the fact is that this simplicity is what gives Lisps so much strength and flexibility, and in particular enable the meta-programming capabilities that Lisp is famous for. e.g. if I want a macro to call several different functions on the same arguments I just do something like the following (not this is not just runtime function application - it's a macro that generates compiled code as if you had written out all the individual function calls by hand):
(defmacro print-many [functions args]  
  `(do   
     ~@(map   
        (fn [function] `(println (~function ~@args)))   
        functions)))

(print-many [+ - * /] [10 7 2])  
19  
1  
140  
5/7
  • As a result of the incredibly simple syntax, Lisp's "code is data" philosphy is much more powerful than anything you can do with function composition / templates / generics etc. It's more than just DSLs, it's code-generation and manipulation, at compile time, as a fundamental feature of the language. If you try to get these kind of capabilities in a non-homoiconic language like C# or Java, you'll eventually end up reinventing Lisp, badly. Hence the famous Greenspuns Tenth Rule : "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp". If you've ever found yourself inventing a turing-complete templating / flow control language within your application then you probably know what I mean.....

  • Concurrency is a unique strength of Clojure (even relative to other Lisps), but if you believe that the future of programming will mean having to write applications that scale to highly multi-core machines, then you really really should look into this - it's pretty groundbreaking. Clojure STM (Software Transactional Memory) works because Clojure embraces immutability and lock-free concurrency constructs based on a novel separation of identity and state (see Rich Hickey's excellent video on identity and state). It would be very painful to graft this sort of concurrency capability onto a language / runtime environment where a significant proportion of the APIs work on mutable objects.

  • Functional programming - Lisps emphasise programming with higher order functions. You're right that it can be emulated in OO objects with closures / function objects etc. but the difference is that the entire language and standard library is designed to help you write code in this way. For example, Clojure sequences are lazy, so can be infinitely long unlike your ArrayLists or HashMaps in C#/Java. This makes some algorithms much easier to express, e.g. the following implements an infinite list of fibonacci numbers:

    (def fibs (lazy-cat '(0 1) (map + fibs (drop 1 fibs))))

  • Dynamic typing - Lisps tend to be at the "dynamic" end of the functional programming language spectrum (as opposed to langaages like Haskell with a very strong and beautiful conception of static types). This has both advantages and disadvantages, but I'd argue that dynamic languages tend to favour programming productivity because of greater flexibility. This dynamic typing has a minor runtime performance cost, but if that bothers you you can always add type hints to your code to get static typing. Basically - you get convenience by default, and performance if you are willing to do a bit of extra work to get it.

  • Interactive development - actually I think you can get some sort of REPL for C# 4.0, but in Lisp it's standard practice rather than a novelty. You don't have to do a compile / build / test cycle at all - you write your code directly in the running environment, and only later copy it into your source files. It teaches you a very different way to code, where you see results immediately and refine your solution iteratively.

A few quick comments on the things you see as "missing":

  • As you say, Clojure has namespaces. In fact, they are dynamic namespaces: you can update them at runtime - this provides some surprising benefits for interactive development. I've had lots of fun redefining functions under the nose of a running thread, or hacking a live data structure....
  • Compile / design time support - not really relevant if you're coding at a REPL - you just do it and see the result directly. Having said that, Clojure is starting to get some improved IDE support, e.g. the CounterClockwise plugin for Eclipse.
  • GUI development - Yes, you'll have to learn a new API, but that's always going to be the case when switching languages.... good news is that the Java APIs aren't that hard and there are now some interesting Clojure-specific wrappers / examples that look pretty easy to use. See this question on Clojure GUI development for some nice examples
  • GUI debugging: this works with the GUI debugger in Eclipse assuming you use CounterClockwise, or Enclojure if you use Netbeans. Having said that, I don't use this capability - I find interactive debugging at the REPL more productive.

I personally find the advantages of Clojure / Lisp pretty compelling, and I'm not planning on going back to C#/Java (beyond what I need to borrow all the useful libraries!).

However, it did take a few months for me to really get my head around it all. If you're genuinely interested in learning Lisp / Clojure as an enlightening learning experience, there is no substitute for diving in and forcing yourself to implement a few things.....

2
  • 1
    Thanks for the great enumeration of ideas. I actually do use lazy sequences a lot via the IEnumerable<> construct in C# (and haven't touched an ArrayList since 2005 or so) but I understand the idea. Clojure's concurrency primitives are extremely impressive. I was doing grid computing in C# a few years back -- concurrency across many multi-core machines -- and I still believe that's the future. Probably the biggest idea that I've gotten from everyone is "you really have to just dig in and experience it, it can't really be explained". So, I'm going to keep digging and keep experiencing.
    – Jonathan Mitchem
    Commented Feb 21, 2011 at 22:30
  • 1
    Cool, glad to have helped - and that's absolutely the right spirit!!
    – mikera
    Commented Feb 21, 2011 at 23:49
10
votes

C# has got a lot of features, but the mix feels different. That some language has a feature still does not mean that it is easy to use, paradigmatic and well integrated with the rest of the language.

Common Lisp has this mix:

  • s-expressions as a data syntax
  • code as data leads to macros and other techniques of symbolic computation
  • dynamic language allows runtime modifications (late binding, MOP, ...)
  • strongly typed, dynamically typed
  • interactive use with incremental compiler or interpreter
  • Evaluator as core execution engine
  • managed memory with Garbage Collection
  • Hybrid language with heavy use of imperative, functional and object-oriented programming. Other paradigms can be added (rules, logic, constraints, ...).

Now, other languages, like C#, have that, too. But often not with the same emphasis.

C# has

  • C-like syntax
  • mostly imperative object-oriented, with some FP features
  • statically typed
  • mostly batch use with a batch compiler
  • managed memory with Garbage Collection

It does not help that C# has for example code manipulation features, if they are not widely used as in Lisp. In Lisp you won't find many software that does not make heavy use of macros in all kinds of simple and complex ways. Using code manipulation is really a big feature in Lisp. Emulating some uses is possible in many languages, but it does not make it a central feature as in Lisp. See books like 'On Lisp' or 'Practical Common Lisp' for examples.

Same for interactive development. If you develop a large CAD system, most of the development will be interactive from the editor and the REPL - directly into the running CAD application. You can emulate much of that in C# or other languages - often by implementing an extension language. For Lisp it would be stupid to restart the CAD application under development for adding changes. The CAD system would then also contain an enhanced Lisp that would have added language features (in form of macros and symbolic descriptions) for describing CAD objects and their relationships (part-of, contained, ...). One can do similar things in C#, but in Lisp this is the default development style.

If you develop complex software using an interactive development process or your software has a substantial symbolic part (meta programming, other paradigms, computer algebra, music composition, planning, ...), then Lisp may be for you.

If you work in the Windows environment (I don't), then C# has an advantage, since it is a main development language of Microsoft. Microsoft does not support any form Lisp.

You write:

  • Namespaces. Even with static methods, I like to tie them to a "class" to categorize their context (Clojure seems to have this, CL doesn't seem to.)

Common Lisp does not attach namespaces to classes. Namespaces are provided by symbol packages and are independent of classes. You need to reorient your approach to object-oriented programming if you use CLOS.

  • Great compile and design-time support the type system allows me to determine "correctness" of the datastructures I pass around anything misspelled is underlined realtime; I don't have to wait until runtime to know code improvements (such as using an FP approach instead of an imperative one) are autosuggested

Use the Lisp compiler. It informs you about unknown variables, might have style recommendations (depending on the compiler used), gives efficiency hints, ... The compiler is a keystroke away.

  • GUI development tools: WinForms and WPF (I know Clojure has access to the Java GUI libraries, but they're entirely foreign to me.)

The commercial Lisps like LispWorks and Allegro CL offer similar things under Windows.

  • GUI Debugging tools: breakpoints, step-in, step-over, value inspectors (text, xml, custom), watches, debug-by-thread, conditional breakpoints, call-stack window with the ability to jump to the code at any level in the stack (To be fair, my stint with Emacs+Slime seemed to provide some of this, but I'm partial to the VS GUI-driven approach)

The commercial Lisps like LispWorks and Allegro CL offer all of that under Windows.

11
  • 1
    My criticism isn't really of Lisps. I find them to be quite capable. I'm looking for what can a Lisp do/give-me that I don't already do. I fully understand that most C# devs don't use many of the "new" language features. I think the larger point is that I do, and honestly, outside of the GUI, I almost write in a 100% FP style. FP was a conceptual leap, but worth it. However, switching to a Lisp from where I am seems to provide little benefit. I'm hoping to find that there's a reason I should keep giving it a chance.
    – Jonathan Mitchem
    Commented Feb 20, 2011 at 16:58
  • @Jonathan Mitchem: C# does not really look like an FP language, even if the language has some features supporting it and a few libraries might use those. If you want to do FP programming in the Microsoft eco-system, then F# might be for you.
    – Rainer Joswig
    Commented Feb 20, 2011 at 17:06
  • @Rainer No, it doesn't look like an FP language, but it can do it, and fairly compactly at that. And when I want/need imperative code, or OO, I can do that too. (slightly off-topic: I don't really consider F# a serious language worth digging into, I'd rather get my head around monads in Haskell. But when I was a C++ dev and C# came out, I didn't consider it a "real" language either [and back then it wasn't, imho])
    – Jonathan Mitchem
    Commented Feb 20, 2011 at 17:15
  • @Rainer Here's an example of FP in C# jmitchem.livejournal.com/#post-jmitchem-69978 also jmitchem.livejournal.com/?skip=10#post-jmitchem-66618
    – Jonathan Mitchem
    Commented Feb 20, 2011 at 17:30
  • @Jonathan Mitchem: why should I look? I wrote that C# supports some FP features. It just added to an imperative object-oriented language and does not make it idiomatic, as in, say, Lisp, Scheme and especially not compared to SML, F#, Haskell or other mainly functional languages. My point is: some FP is possible in C#, but it is not a core feature.
    – Rainer Joswig
    Commented Feb 20, 2011 at 17:43
5
votes

Rainier Joswig said it the best.

For me, the power of Lisp can't be understood by just looking at a list of Lisp features. For Lisp, and Common Lisp in particular, the whole is greater than the sum of the parts. It's not just the REPL plus s-expressions plus macros plus multi-method dispatch plus closures plus packages plus CLOS plus restarts plus X, Y and Z. It's the whole shebang all ingeniously integrated. It's the day-to-day experience which is very very different than the day-to-day experience of other programming languages.

Lisp looks different, it is used differently, one approaches programming in a different fashion when using it. It is elegant, complete, big and seamless. It is not without its own blemishes and peccadilloes. There are certainly comparisons to other languages that might be made where it might not come out ahead. But in no way is Lisp just language X plus REPL and macros, or language Y plus multi-method dispatch and restarts.

3
votes
Code is Data (and Macros) - Maybe I haven't "gotten" macros, but I haven't seen a single example where the idea of a macro couldn't be implemented as a function; it doesn't change the "language", but I'm not sure that's a strength

Typically, a macro should be used for something that is not expressible as a function call. I don't know if there is a switch-like conditional in C# (similar to what exists in C, as switch (form) { case ... }), but let us assume it does and has close to the same restrictions (requiring an integral type).

Now, let us further assume that you'd like something that looks like that, but dispatches on strings. In lisp (well, specifically in Common Lisp and probably in others), that is "just" a macro away and if you restrict the user to having constant strings (probably not arduous) to match against, you can (compile-time) compute a minimal sequence of tests to determine what case matches (or use a hash table, storing closures, maybe).

While it may be possible to express that as a function (comparison string, a list of cases and closures to execute), it would probably not look like "normal code" and that is where lisp macros have a definite win. You've probably used quite a few macros or special forms taht are macro-like, like defun, defmacro, setf, cond, loop and many more.

2
votes

This is the best explanatory article I've found that takes the reader from the surface of Lisp advocacy to show some of the deeper implications of the concepts being used:

http://www.defmacro.org/ramblings/lisp.html

I recall reading elsewhere an idea like this: Other languages have adopted various features of Lisp; garbage collection, dynamic typing, anonymous functions, closures to produce a better C++/C/Algol. However to get the full power of Lisp you need all of its essential features, at which point you have another dialect of Lisp, a distinct family of languages that has been around in one form or another for over 50 years.