Hacker News new | ask | show | jobs
by 55acdda48ab5 3686 days ago
Talking about lisp in 2016 is mostly just debating static vs. latent/dynamic typing. The lisp guys were talking up their advantages in the 80s/90s era of fortran and C and really bad c++, and I guess the really shitty original JVM. It's a discussion from a dead age.

latent/dynamic typing and also macros work very poorly when the codebase is large and there are many people involved, or when we're talking about decade plus code-base lifespans. It's that simple. If you're one or three noticeably smart dudes building a system from start to ultimate finish (financial exit in four years?), why not go with LISP or something like it. If you're a team of one or two I advise you do go with lisp or perl or python or erlang or whatever.

But that's not the systems anybody builds or maintains much anymore. We make things that a rotating cast of 100 might touch over 30 years. We need static typing.

3 comments

I disagree on several points:

> latent/dynamic typing and also macros work very poorly when the codebase is large and there are many people involved, or when we're talking about decade plus code-base lifespans.

Lisp is one of the few languages that can say it actually doesn't age; Common Lisp code that was written 20+ years ago is often used today without a single change. You can't say that about most of the popular languages.

RE many people on the team - I see a lot of talk about how macros can be unreadable and all, but frankly, IMO that's totally backwards. Readable code is not about using a subset of language that you can find in "X for Dummies" book. Readability is about structuring your code to express intent, to be logically consistent, and about all the other things that transcend the syntax of the language. Macros are an ultimate tool for increasing readability, because you can keep recursively eliminating boilerplate, cruft and repetitions, bringing your code closer and closer to the intent it's meant to communicate.

> But that's not the systems anybody builds or maintains much anymore. We make things that a rotating cast of 100 might touch over 30 years. We need static typing.

Static typing is cool and all (I like it), but RE systems - no, it was in Lisp age people actually cared about buildings systems that would live for decades. Today, people build temporary systems that get thrown away or rewritten every couple of years at most.

> Lisp is one of the few languages that can say it actually doesn't age; Common Lisp code that was written 20+ years ago is often used today without a single change. You can't say that about most of the popular languages.

Examples?

This is certainly false for most languages in use today: C, C++, Java, even C#: code written in these languages 15-20-30 years ago can still be compiled and run fine today.

I'm not sure what this proves much, though.

> I see a lot of talk about how macros can be unreadable and all, but frankly, IMO that's totally backwards.

Why?

Macros are basically syntax defined for a specific task. Why is it so hard to see that this can lead to an explosion of unreadable code if left unchecked? Wouldn't you be concerned if you had to work on a huge code base where most of the code is written using macros?

I would run away, personally.

> it was in Lisp age people actually cared about buildings systems that would live for decades.

We still care about this today. Even more than in "Lisp age" because we know how long code will be around. Which is one of the reasons why we have been moving at an accelerated pace toward statically typed languages.

> Examples?

Half of the libraries in the Lisp ecosystem? They were done once, polished over years, and pretty much did not age with time.

> Why is it so hard to see that this can lead to an explosion of unreadable code if left unchecked? Wouldn't you be concerned if you had to work on a huge code base where most of the code is written using macros?

Because again, readable code is not about using the same small subset of programming language constructs and design patterns everyone knows. It's about clear communication. Macros done right let you express your ideas more clearly, and hide/remove unnecessary boilerplate that makes code hard to read. Think about e.g. Java or C++ codebases, where 50%+ code is scaffolding and otherwise irrelevant to what the program is meant to do / communicate. Lisp macros let you hide all that.

Now if you do macros wrong, then of course code will be unreadable. But the same is when you design your API wrong using functions, or using classes.

Moreover, whining about macros being hard reminds me of whining about ternary operator in C++/Java/PHP world, where many people say not to use them because "juniors don't understand it". The solution isn't to ban ternary operators - it's for the juniors and the whiners to get their shit together and spend 5 minutes learning about it.

> Even more than in "Lisp age" because we know how long code will be around. Which is one of the reasons why we have been moving at an accelerated pace toward statically typed languages.

Do we? All I see is throwaway code. Especially on the Web, everything is ephemeral, and nobody honestly expects stuff to last more than few years (most startups are actually based on this assumption).

> Why is it so hard to see that this can lead to an explosion of unreadable code if left unchecked?

Anything can lead to an explosion of unreadable code if left unchecked. Any language feature, with no exceptions. Variables, loops, functions, types - you name it. Anything can go wrong if used with a bit of imagination.

Macros, on the other hand, provide an exclusive way of eliminating this complexity. A way of eliminating degrees of freedom of what can go wrong. Nothing else is capable of doing it.

> Wouldn't you be concerned if you had to work on a huge code base where most of the code is written using macros?

I'd be extremely happy to be able to work on such a well designed project.

> I would run away, personally.

It only means that you don't know how to use macros. Nothing else.

> I'd be extremely happy to be able to work on such a well designed project.

You really think the project is well designed just because they use macros, without even looking at the code or knowing the engineers?

Now I really think you're not for real and just messing with us.

I think that it can at least be a sign of a good design. If it went that far and still kicking, chances are pretty high.

Any comments on any other points I made?

I completely agree and I wish we could have the best of both worlds. I love Clojure and I love the compiler support with static typing in other languages.

Clojure attempted to add static typing with the core.typed projects, but high-profile exits [0] from that framework have made clear that this is a problem that can only properly be implemented at the language level.

If I could have a lisp with static types, I might not use anything else again.

[0] https://circleci.com/blog/why-were-no-longer-using-core-type...

Common Lisp has static types.
But not good parametric polymorphism. Common Lisp's static types are "too static", and often unsafe.
Ok for parametric polymorphism: the identity function in OCaml has type 'a -> 'a, whereas in SBCL it is "(function (T) T)". On the other hand, "(lambda (x) (if x 0 1))" is a function from T (anything) to the type BIT, not to some arbitrary integer type. Moreover:

    (lambda (x)
      (unless (minusp (if x 0 1)) 
        (error "Oops")))
... has type "(function (T) NIL)", meaning that the function does not return a value. That means that types are propagated so that the test can demonstrably always fail. A type in SBCL defines what is returned when execution terminates normally. So for example the type of `(lambda (x) (loop))` is "(function T NIL)", because it never returns (yes, I know you cannot always tell if it halts or not). The bottom type NIL should not be confused with NULL, the singleton type for the NIL value.

In OCaml, exceptions are not visible by the type system and you can write:

    let f x = raise (Failure "NO")
... and still have the type 'a -> 'b

So the kind of analysis that make sense in a language, as well as their soundness, is relative to the properties you want to check. Would you say that OCaml type system is unsound because it allows you to run code that can raise exceptions at runtime? I would love to see more precise type checking in OCaml, for example, and it probably already exists (I'am interested, if anybody has an example). But it probably makes little sense over there.

The same goes for parametric polymorphism in Lisp. The most in-depth approach to bring parametric polymorphism in CL is LIL (https://common-lisp.net/~frideau/lil-ilc2012/lil-ilc2012.htm...), but since it is dynamic, people who view dynamic typing as a deficiency might see that as a restriction.

You also claim that the type system is "unsafe". On the contrary, types being checked at runtime is a safe approach (buffer overflow, etc.) and plays well with the fact that everything can be redefined at runtime (maybe you don't like this aspect). In SBCL, type declarations are assertions. With the default optimization levels, that means that if they cannot be proved, they are checked at runtime (with the few caveats listed in SBCL man page). So if your input variable X is type as NUMBER and the function terminates normally, then you know that X effectively was of type NUMBER (that's a guarantee instead of an assumption). That result can be used in the following calls so that checking the type of X is not necessary anymore (if it is not modified in the meantime). The only case where types are trusted blindly instead of being checked, when necessary, is when you set the safety level to zero. This can be changed locally, not necessarily as a global switch.

Run time type checks in my experience mean a few things:

1) You can compile or run the program fine even if there are type errors.

2) An existing type error may never be caught if that particular block of execution never runs. Then you can have unexpected surprised later when you finally do something to trigger that block.

3) They are slower than static type checking due to the runtime costs of checking types. Statically-typed compilers can do enormous optimizations once they know exactly what the types are.

I am fine with statically checking types, or any kind of proof. Are your floating points calculations always precise enough? Can you detect if no user inputs ever reaches critically secure code without being sanitized? Does this multi-threaded code ever deadlock? There are plenty of things that can be made to guarantee properties you care about, and the reason you do not always use them is because of budget (time, money). Do you always write contracts around your stuff? is it a good contract, not one that just repeat what your code does? If you don't, please understand that I don't always use static types for everything and in all cases. Also, my code is not purely functional either.

If I need to do smart things in Lisp ahead-of-time, I may want to use a DSL and prove whatever I want to prove on it, and generate correct-by-construction code that does not check things at runtime. I could use ACL2, too. Please note also that I can work in a different language when it makes sense.

3) Statically typed compilers include Lisp ones. My code is sometimes underlined in orange like yours, because I made some dumb typo or because types do not match. Likewise, optimizations are done too, in particular inside functions, where things are more static than in the global environment.

What about efficiency? Look at the postal system: you have to wrap letters and objects in enveloppe or packages (except postcards, which are like fixnum), put a label on it with many informations... what a waste of time and space! Yet, each object has a type now and can be dispatched reliably and efficiently. If you put your letter in the wrong box, you will have enough information to recover and perform your job. Once letters are all filtered out from packages, you can avoid checking that they are letters and gain some efficiency. You can build a new kind of service (drone-delivery?) with a special label and dedicated rules and integrate it within a running system without restarting the world. If anything goes wrong, you can have a generic error handling mechanism that does not crash everything. Static types are more like pipes: clean water here, used water there, and they never mix in a wrong way thanks to pipe calculus. So while I agree that static analysis can be great for doing crazy optimizations, there are use-cases where dynamic typing shines, and generally people replicate that using tagged objects anyway: think about game entities which are "typed" dynamically, or frameworks when you can load custom scripts (and those are generally not typed-checked, unlike Lisp).

2) You plan for failure. Even in a statically-type language, you'll have runtime bugs. Only in a mission-critical system are errors fatal. If you place restarts or error handlers accordingly, you'll have the opportunity to fix your stuff. Ask Erlang people about reliable systems.

1) Worse, you compile your code, the compiler complains but you can still run the produced code! Then you can test your error-handling code.

Seriously, this is not a problem in practice. Coding and testing are interleaved because the environment is right here awaiting orders. If you look at the end-result, I am doing the job of the static type checker by fuzzying input in a way that makes sense for that particular function. I also have a global view of the system and more context to decide what will happen at runtime, or not. When I fail (I am not proud of it, like most static analyzers), the runtime is here to catch it.

And Racket. And Shen.
Irken looks interesting, thanks for the link.
Javascript though.
In both cases of Javascript and Objective-C, programmers didn't deliberately choose "dynamic" as an explicit engineering design.

Even though the Apple System9/OSX platform has been around a long time, the Apple ecosystem got really popular with the iPhone & iOS in 2007. From 2007 to 2014, the official SDK for that was Objective-C. Obj-C is dynamic, and as a consequence, people happened to program in a dynamic language. ("When in Rome do as the Romans...")

Same situation for web browsers. The only "sdk" for adding interactivity and actions to web pages was Javascript -- which happened to be "dynamic".

In other words, "dynamic" was something programmers had to live with rather than something they chose for architectural superiority.

In both cases (Swift, TypeScript), the desire for static typing became apparent.

People are constantly complaining about using it. And many of them are trying to make it safer, it's just a slow process since it requires standardization and adoption at a large scale. Think about strict mode, ES6, Google Closure compiler, Flow, Typescript, etc.