For some reason I was thinking about Bob two days ago, ten years since I last interacted with him. I was just an intern at Square and he had just joined as CTO. But I remember how patient and inquisitive he was with me. Rest in peace, Bob.
In this case, you think of `connectWords` as a function that takes two arguments. But since it is curried, you can also do this:
let prefix = connectWords "Hello "
world = prefix "world"
bob = prefix "bob"
in ...
`world` is "Hello world", and `bob` is "Hello bob". That is the power of currying. A maybe more useful example is specifying the mapping function in `List.map` without supplying the list to map over. This allows you to use the same map with multiple lists.
In the article, I mention that linters for dynamic langs are crippled. Of course I use them when writing Ruby. But can any of these tools tell me:
- When two equivalent anonymous functions can be extracted out?
- When a library method already exists for an expression?
- When you fail to match every possible result in a case statement?
Yes, I may have failed in getting it across, but a big point is this: I don't think dynamic languages will suddenly die out. Instead, strongly-typed languages will become more convincing. We are seeing better tools, and they are becoming more ergonomic.
Elm's latest blog post said it really well:
> Compilers should be assistants, not adversaries. A compiler should not just detect bugs, it should then help you understand why there is a bug.
A note on Typed Clojure: Ambrose is doing excellent work with it, and I'm excited to see it develop.
But I wished there was a bigger push from the top, from Rich himself, on "first-classing" typed Clojure. Aim for 100% annotation coverage for the most popular libraries. Start with core (maybe it already is? I haven't kept up) and ring, then move outwards from there.
Library writers will be more motivated to add annotations if the big libraries are doing it.
I doubt Rich will ever push for it, but stranger things have happened. Types get in the way of data. When you ask a request "What's in you?" in Clojure you can just look at the data and find out. In Java you can ctrl+space in your IDE and look for public methods that by convention start with 'get', and then for the return values of those things also look for things that start with 'get', but that's a pretty impoverished way to get at the data. Try printing it out and you're likely to just get a memory address, great. So what do types buy you? Sometimes performance, but if that were always true then all static languages should be about as fast as C. The other thing is this "certainty" mentioned, and that certainty is just that you didn't make a typo or spelling error, woop-dee-doo, those are among the fewest sets of errors dynamic programming language users run in to, especially when you change your workflow to take advantage of the nature of dynamic languages, and a more valuable certainty is that this piece of data you have isn't going to change underneath you. You have to go all the way to Haskell (I'd argue Shen) to get real benefits of typing beyond performance and typo-protection, and most programmers are unwilling to do that for very good reasons.
I fear I must be misunderstanding your points. Are you really stating that, in order to know what you can do with data, you need just need to inspect it at runtime? Or that types only protect you from typos?
Types allow the compiler to do a lot of validation before your code is even executed, removing whole classes of bugs before they even have a chance to manifest themselves. You thought that value was an int and divided it by two? It was a string. I'm glad I didn't have to wait until runtime to find out about it, or to write tests to make sure that every single code path to that division results in the value being, in fact, an int.
Types allow developers to trust whatever data they receive without feeling the need to protect against hostile, or less experienced, application programmers passing incorrect data. They allow you to know, with absolute certainty, that what you wrote can only be executed the way you meant it to be executed (whether that's correct or not is another question altogether). A function in a dynamic language is never complete, you never know what data, or shape of data, you will receive. Types buy you that certainty.
Code efficiency is not the point - developer efficiency is. When you can trust your data, you can focus on what your code needs to do - that's usually complicated enough without adding the complexity of not being able to trust a single value.
The main argument against static languages is that, in their struggle to be sound, they end up not being complete - and it's absolutely true: there are perfectly legal programs that you can't write with a static language, but that a dynamic one would run without batting an eyelash. Duck-typing comes to mind, I'm sure there are other examples.
Another argument is that type systems get in the way of what you want to do. There are a few cases where it's true - see the point just above. But in my entirely anecdotal experience, the vast majority of times someone complains the compiler won't let him do what he wants, it's because what he wants to do is not correct. The complaint is not that the compiler is too restrictive - it's that your bugs are shoved in your face much, much more frequently and quickly than they would if you had to wait for runtime. And that's a good thing.
"Runtime", or "REPL" time, or "doc time" are all ways you can just "look at it". Do you think knowing "x" is type "Foo" tells you anything about what you can do with "x", other than use it where you can use "Foo"s? You need to figure out what you can do with "Foo" by reading the docs around "Foo". Yeah, I know a priori I can call a method on Foo that returns an int and pass that to other int-taking methods, but is that what I want to do? Which method returning an int do I want to call? You need to look at what "Foo" is to determine this. Rich's example is HttpRequestServlet in https://www.youtube.com/watch?v=VSdnJDO-xdg I'd recommend watching that and ignoring me. A lot of the issues he raises, as contextfree points out, are around OOP rather than static typing in general so take these as my views and not necessarily his...
If "Foo" is just dynamic data, perhaps in a map, composed of other data, and it's values all the way down, any of those three points of time are easy ways to look at "Foo" and can generally be faster than reading a JavaDoc page or ctrl+spacing figuring out what you have access to and what it all means and what you want to do with the data and code associated with "Foo". Since you're going to be looking at the thing you want anyway, I find it very uncommon in practice that I accidentally divide a string by 2. Static typing would let me know before I run the thing that I was an idiot (maybe, see next paragraph), but I'll find out almost as quickly when the code I just worked on (either in the REPL, which is very nice, but even from the starting state) actually does what I want -- you do that regardless of static/dynamic typing, right? You don't just write code and never verify it does what you want, trusting that it compiled is good enough? Anyway when I run it, I'll immediately hit a runtime error (which can be handled in Lisp in ways way more flexible than exceptions in other languages) and realize my stupidity. So I don't find that particular situation or class of bugs very compelling.
Nor do the presence of declared types alone guarantee my safety -- I need to know about any conversion rules, too, that may let a String be converted to an int for division purposes, or if there is operator overloading that could be in play. Conversion rules and operator overloading are interesting regardless of the static/dynamic typing situation because too much or too few can lead to annoyances and bugs. Java lets you concatenate ints into strings with "+", Python doesn't, but Python lets you duplicate strings with "*".
What I have run into in both static and dynamic languages is division-by-zero errors, which, unless you go all the way to something like Shen that supports dependent typing, your type system with "int" declarations will not help you with. I've run into errors (in both static and dynamic languages) parsing string contents to extract bits of data. I've run into errors around the timings of async operations. I've run into null pointer exceptions (though much less frequently in dynamic languages, maybe once the Option type is in wider use I can rely on that too but its issue is poisoning the AST). I've run into logic errors where I thought the system should be in state C but instead it transitioned to state E. The bugs that take the longest to diagnose and fix are simply due to not understanding the problem well enough and having an incorrect routine coded, so I have to figure out if it's a logic error or an off-by-one error or simply using the wrong function because it had a suggestive name and I didn't read through the doc all the way. Rich made an interesting point in one of his talks that all bugs have both passed your type checker (if you have one) and your test suite. My point is that I'm not convinced static typing as I'm given in the languages I work in is worth it for any reasons beyond performance because I don't see any other improvements like making me more productive or writing less bugs. I'm not opposed to types and schemas themselves in general, just static typing. I'd rather see pushes towards full dependent typing and systems like TLA+ (which has seen more industry usage than Haskell) and Coq than things like core.typed in Clojure.
The last point I'd raise is that I agree with you developer efficiency is more important than code efficiency. But this has been one of the mainstay arguments from the pro dynamic typing camp for many decades, with static typing advocates conceding that yes the developer will be more productive in a dynamic language but the performance is terrible for anything serious. If you want to argue static typing was actually more efficient all along from a developer perspective than dynamic typing, you have a lot of work to do. I'm not particularly interested in that argument (at least today) but the point still needs to be raised.
"You have to go all the way to Haskell (I'd argue Shen) to get real benefits of typing beyond performance and typo-protection"
With some cleverness, I was able to get C's type checker to let me know when I was accessing data from the wrong thread. This was tremendously useful when moving functionality between threads. If you think type checking doesn't buy you much, it's because you don't know how to use it.
your "just looking at the data" vs. looking for "get" methods is really about abstract data types v. procedural data abstraction (aka OOP, representing the data as a bunch of methods), not so much about untyped v. typed. you could represent data in an untyped language as a tuple of getter functions and the problem would be even worse there.
Yes, I did miss the ring spec. But as a beginner to ring, I'm not sure how I was suppose to know that such a document exist.
For the most part, Ring is well-documented. But, as you noted, human-typed doc strings go only so far. I applaud the writers of ring in their discipline, but certainly we can do better than depending on human discipline.
You even noted, I picked "less well-documented libraries." I didn't pick them because they were not well-documented—I picked them because I'm using them!
I suspect you might have encountered the SPEC if you opened the README of the ring project or visited its github page (which displays the README by default). It's mentioned and linked in that README:
Ring is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.
The SPEC file at the root of this distribution provides a complete description of the Ring interface.
From my (short) time with Haskell, I've found the type system to be the best I've ever used. It really does feel like it's helping you, instead of getting in the way. And so it feels more like a dynamic language than other strongly typed languages I've used (e.g. Scala, Java).
Below is a simple example, but look, no types to type!
>> let plus1 = (+1)
>> plus1 10
11
>> :t plus1
plus1 :: Num a => a -> a
You can ask for the type by `:t plus1`.
I've also used Clojure, and I do really like Lisps, but I don't think untyped languages give enough benefit for all the problems it causes. I'm excited, however, about Typed Clojure (and Typed Racket).
If I understand correctly, the ClojureScript compiler was written in Clojure (that is, you had to run the JVM to run the compiler), which spat out JS code and then compiled by Google Closure.
Would you be able to explain, at a high level, how ClojureScript bootstrapped itself, and how Google Closure comes into play?
Over the years the language differences between Clojure & ClojureScript have more or less been erased beyond very host specific details like String methods, no multithreading, etc. Clojure 1.7.0 landed reader conditional which permits writing conditional code based on whether the file is being compiled by Clojure or by ClojureScript.
So we simply conditionalized a few key things in the analyzer and then we just point ClojureScript JVM back at its own sources. This part was actually very simple to do. However much more challenging is the issue of macros. As with many Lisps, Clojure has only a few primitive special forms and most of the language is actually written in terms of macros. ClojureScript is no different, the macros files is longer than the analyzer or the compiler.
Google Closure was always an additional optimization pass. The bootstrapped code we generate is still Closure compatible making it easy to run it yourself seperately as an optimization pass or to produce a single JavaScript file you can put somewhere on the Internet if you like.
One way to look at it: Replete has a bit of CLJS source which requires the reader, analyzer and compiler namespaces. When Replete's source is built (using the JVM), these additional namespaces get compiled down to JavaScript and are usable by Replete itself, in the R and E parts of the REPL. It is all JavaScript, of course, when on the device.
Google Closure is used for its dependency management (ClojureScript namespaces and how they require other namespaces). But Google Closure is not used for its optimizations (everything is built with :none mode).
Yes the ClojureScript compiler is written in Clojure, more than that it is written in a special form of Clojure that can do conditional branching depending on what environment it is running in. So now the ClojureScript compiler can be ran in ClojureScript.