Hacker News new | comments | show | ask | jobs | submit login
First look at Clojure.core.typed (logaan.github.io)
135 points by fogus 1571 days ago | hide | past | web | favorite | 57 comments

Since no one has said anything about it -- I really admire the willingness to post a recording of oneself attempting to use someone else's code. A lot of people would shy away from this because viewers might have arbitrary unrelated criticism ("you type so slow!"). This type of thing could be really useful to authors who can notice if a certain concept isn't grasped right away or if documentation didn't immediately solve someone's need.

This is used regularly in UX labs with two-way mirrors and cameras watching how users interact with design -- why should it not be useful to programming as well?

Ooh god now I'm going to be self conscious about my typing. But seriously I was mainly just worried that Ambrose (the brains behind core.typed) might take it the wrong way. Many of the problems I had could've been avoided if I'd just taken my time and read the docs, and I don't mean for my failing to reflect poorly on his library. He took it like a champ though and released a patch fixing a few issues I hit the same night I posted.

A perhaps naive question: could this sort of thing tap into Clojure's type hints rather than using external annotations? So rather than writing the example in the post:

(ann my-inc [Number -> Number])

(defn my-inc [n] (let [internal-number (Integer/parseInt "1")] (+ n internal-number)))

one could write this:

(defn ^Number my-inc [^Number n] (let [internal-number (Integer/parseInt "1")] (+ n internal-number)))

...and then run a type checker that reads and analyzes the hints?

Of course, the core.typed annotations could preserve whatever information they need in the compiled output, whereas the type hints aren't preserved past the compile phase (as far as I can tell.) Is that the limitation that requires defining type information in separate annotations?

Clojure's type hints are only about Java interop and performance, and pretending otherwise is not a good idea. They aren't nearly rich enough to do the things that Typed Clojure does - think about less trivial examples like type checking heterogenous maps.

Ah, I didn't realize that core.typed could deal with rich data structures like that. That's actually pretty amazing.

I wanted something like this, so I made this library that checks annotations at run time rather than compile time.


Right now it only checks maps for a set of keys, the idea longer term is to add in other types of "type" checking.

I haven't yet tied into the built-in annotations, just because those didn't appear to check anything but valid Java types. If it is possible, I would be really interested in seeing how.

For truly typed lisp you should also check Shen. Its type system is built in and very advanced and flexible (based on sequent calculus). You can write programs in both type checked and unchecked mode. One of the supported platforms is also Java.

Project is still young and in an early phase, but definitely promising.

Indeed, I don't really understand how that could be code. Are those just cute symbols to match on?

Yes -- see https://github.com/hraberg/shen.clj/blob/master/shen/src/pro...

Upper case symbols are variables, as in Prolog. The others are literal matches.

Elsewhere in the file he matches on things like "rename the variables": https://github.com/hraberg/shen.clj/blob/master/shen/src/pro...

It looks really cool! Shame about that license though...I can appreciate the fact that he wants a focused community, but he risks not having a community at all.

You'll be happy to know that the currently inherently ambiguous license is being reworked, to something akin to a normal open source license with the GCC runtime exception for users and the Sun's restriction on use of Java trademark, but using copyrights and not allowing the games that caused Apache's Harmony project so much grief.

Damn it - another unique Racket feature stops being unique.

I hoped that in 2020 Racket will be around 5th position in "the most popular language" rankings with Clojure in the first ten too. Now it looks like Clojure is taking the lead - maybe Racket2 will change this again.

But hey, either way that's a popular, mainstream Lisp! That's unprecedented!

Racket remains special at a deeper level, AFAICT. At first glance, it's a wonderful, modern, practical Lisp with many batteries included. Which it is. You can use it as just that. But also, it is a remarkable language for making languages that can interoperate. For example you can freely mix `#lang racket` with `#lang typed/racket` modules, as well as with `#lang my-little-dsl`. The combination of `#lang`s and macros is seriously wicked good.

My own hope is that Clojure will open many more eyes to Lispy ways (including reducing paren-phobia) --- and then many of those people will go on to discover and love Racket, too. Indeed already I see people who seem to appreciate and use both Clojure and Racket. I expect that will increase.

I hope for this too. I really appreciate a "language builder toolkit" part of Racket, although I didn't use it very much yet, I only mixed typed/racket, racket and lazy, which was - as you say - seamless and very impressive. Clojure obviously lacks many features in this area and probably always will be lacking them, because it's not it's focus.

As you say, if Clojure's success helps Racket adoption, then I have no complaints at all. It would be great if people used both Racket and Clojure, one for (almost) seamless interop with C and custom DSLs, the other for interop with Java and concurrency - for example.

I still have hope and believe that Racket will become a successful language. I love Racket and prefer it to Clojure, that's true, but I really hope for Clojure's success too. As I said, they're both Lisps, and having even only one of them "succeed" (in terms of mainstream adoption, they're greatly successful as it is already in their niches) will make me happy. I will be a bit more happy if the one to succeed will be Racket, but I will be overcome with joy when they succeed both. :)

The presence in Clojure of persistent vectors and hashmaps, which are very, very fast, and are used pervasively throughout the core libraries and Clojure code at large, makes it way more interesting to me than other Lisps I've seen. Does modern Racket have anything comparable to Clojure's collection primitives?

My introduction to Lisp, some years ago, was reading The Little Schemer, which I loved. Afterwards, I was very disillusioned when I needed an array and learned the canonical solution was to use mutable arrays and set! (as if adding a bang after its name made it less of a sin). I understand why it's like that: persistent data structures are tricky to implement efficiently, and Scheme emphasizes simplicity of implementation. But it still bums me out.

> Does modern Racket have anything comparable to Clojure's collection primitives?

See: https://news.ycombinator.com/item?id=6300845

Also, yes, there are generic "sequence" operations which you can use to abstract away a type of a collection you use. There are lazy sequences too, although they are not the default. There is also a set of very powerful "for" forms, which are an alternative to recursion when traversing sequences (and can function as sequence comprehensions, too).

> and Scheme emphasizes simplicity of implementation. But it still bums me out.

There is a reason why Racket name was changed from PLT Scheme. Racket outgrow Scheme some time ago and continues to grow still. It is "Scheme with batteries included", which means it's not Scheme anymore. But it makes it a great language for practical applications. It's not that great for embedding, though, while normal Schemes are.

Anyway, I find Racket to be the most impressive language I've seen until now. It could use some more developers working on it, but that's all it needs to become on par with Clojure - or any other language for that matter. This makes it's design truly impressive, in my opinion at least.

Standard datatypes and generics (including hash tables, dictionaries, vectors, sets, sequences, streams, etc.):


Additional datatypes (including imperative queues, growable vectors, orders and ordered dictionaries, splay trees, skip lists, interval maps, binary heaps, integer sets, bit vectors, etc.):


Are all of those standard datatypes thread-safe? If not, they are not what the parent was asking for. Clojure's maps, sets and vectors are all thread-safe due to being immutable. They're all built-in to the standard library and have extremely slick syntactic sugar.

Sure, you could implement all of this in Racket, I'm not saying you couldn't; the advantage with Clojure is that these things are standard and that everybody else's libraries make use of this stuff.

This is probably the biggest advantage with starting any new programming language: you get a chance to redo the standard library (and tie it into some syntactic sugar) without worrying about compatibility.

> Are all of those standard datatypes thread-safe?

Some of them have both mutable and immutable versions (lists, hashes) where immutability is the default, some are purely functional (Okasaki's data structures implementation https://pkg.racket-lang.org/info/pfds), and some are mutable by default (the "data" package). Most of the stdlib is immutable by default or plain immutable. There is no "slick" syntactic sugar for these by default, but Racket being as good in "language creation" department as it is, there are 3rd party implementations (https://github.com/greghendershott/rackjure).

> the advantage with Clojure is that these things are standard and that everybody else's libraries make use of this stuff

For the most part it is true for Racket, too. Where it differs is syntactic sugar and more complex data structures; I think some of this will change in racket2 (which is being planned if I understand correctly).

> you get a chance to redo the standard library (and tie it into some syntactic sugar) without worrying about compatibility.

That's why every "racket" program starts with a #lang specifier. Racket, in reality, is many different languages at once; because of this design and matching implementation, there is absolutely no problem with redoing stdlib and syntax completely - you can write Racket that is lazy, that is FRP, that is Algol(try it, it's fun!) that is typed or is logic oriented (http://en.wikipedia.org/wiki/Datalog) already. And you can mix modules written in them freely and conveniently (provide in one and require form in the other module are all it takes in practice).

This alone makes Racket so much more flexible than any other language. What Racket lacks now to implement everything Clojure (and other languages) offers as one of the Racket languages are only man-hours of development team. Other, less complicated features from other languages are being ported all the time, for example generator function in a Python style (with yield) implementation is in a stdlib since a long time, so I believe assimilating proven concepts from other languages is both possible and a "Racket way".

Yeah, it really sounds like Racket is the most flexible programming language out there due to its metalinguistic abstraction capability. The question is: what is the secret to attracting developers to join the community? Clojure seems to really benefit from the rather iconic personality of Rich Hickey; this is in common with other communities and their leaders.

The other thing I wonder is if there is such a thing as too flexible? Rich deliberately resisted adding user-definable reader macros because he wanted Clojure to have a consistent, identifiable syntax. Whether that benefits the language's adoption or not, I have no idea.

I have to say I am hopeful that more people are exposed to Lisp languages on all fronts and that it cross-polinates all of the communities. We all have much to learn from one another and a great deal of potential for innovation. Despite how old Lisp is it still seems like we've barely scratched the surface of its potential.

Though I can't speak from personal experience, I do get the impression that many past Lisps suffered from too much flexibility, in the sense that no one could agree what object system to use, what libraries to use, etc. It seems that Clojure is doing much better that regard, which could partially explain its success.

> what object system to use, what libraries to use, etc.

But this is easily solved by just having a standard library with preferred versions of these things. Many Lisps in the past, and especially Scheme, officially defined very little besides the core language. This is changing, I hear that the next, 7th revision of Scheme standard is going to have two parts, one for the core, and another for the batteries which should be included.

But! Racket comes with it's own selection of libraries in stdlib, which is richer than any other Scheme implementation. So it's not that different from Clojure in this regard.

Racket has reader macros, but it is necessary for it to have them, considering that part of it's focus is to be able to build new languages easily. I haven't seen them used outside of defining these new languages, so I doubt it is a common problem for new users.

There are few things where Clojure and Racket differ. One is the runtime system used - Racket has it's own virtual machine implementation with the JIT while Clojure uses those from Java. This means that a) Clojure devs don't have to spend time on a VM and can focus on the language and b) interop with Java is very strong point of Clojure. Racket on the other hand has a very nice FFI story for C, which makes writing bindings to C libraries very easy.

Clojure is being developed with concurrency in mind and every aspect of a language reflects this. Racket includes support for concurrency as one of many features and so the defaults are not always suited for it and there are things Racket doesn't have. The reason for this is that the Racket developers don't have enough time to bring native threads to VM and rewrite some of the language and library and maintain normal development at the same time. (But there is racket2 planned, which may be a language with most of the Clojure features in it)

And of course Clojure is being much better marketed. Clojure has a few simple selling points and they are being advertised everywhere; Racket is really very complex thing with many man-decades of research behind it and even where it is being advertised, it looks like a typical "academic" thing.

So these are the reasons I think made Clojure more popular than Racket. But I really believe that, in the long run, Clojure's popularity will help Racket, too - it's much smaller jump for people to make from Clojure to Racket than from almost any language to Racket directly. So I really believe that Racket will become successful some time in the future.

Meanwhile, the only thing I can do, is to comment on every Lisp article with reference to Racket and continue hacking in it happily :)

> Racket is really very complex thing with many man-decades of research behind it and even where it is being advertised, it looks like a typical "academic" thing.

As a fellow Racketeer, I'm curious: what do you think can be done to shake this "academic" image?

More people openly sharing good experiences of building stuff.

Clojure's persistent vectors and hashmaps are both the same data structure, the hash array mapped trie, which can be implemented both simply and efficiently. If you look at the Java code in the Clojure source, there are no tricks, beyond the cleverness of the HAMT itself.

You can take solace in the fact that Typed Racket actually does optimizations based on the types. Typed Clojure is more like a linter, AFAICT.

Typed Racket does some optimization based on types and the feature is completely orthogonal to type checking. If Typed Clojure is a linter then so is the type checking phase of every typed programming language.

Ambrose explicitly likened Typed Clojure to Erlang's Dialyzer on the Clojure mailing list. The description of Dialyzer on erlang.org, "a static analysis tool that identifies software discrepancies such as type errors, unreachable code, unnecessary tests, etc.", strikes me as lint-like.

It may be true that the type-checking phase of every typed programming language is, in this respect, lint-like. A typed language's implementation is obviously not required to stop with ensuring that the types check out. It can use the information so derived to do further things, including introduce optimizations.

Insofar as Typed Racket does optimizations based on types it's hard to see how it can be completely orthogonal to type checking, on pain of the optimizations being potentially incorrect; insofar as the optimizations are based on types, it's a use of a type system beyond what Typed Clojure currently offers.

MY MISTAKE: it wasn't Ambrose but someone else (Michael Klishin) who compared Typed Clojure to Dialyzer.

>I hoped that in 2020 Racket will be around 5th position in "the most popular language" rankings with Clojure in the first ten too. Now it looks like Clojure is taking the lead - maybe Racket2 will change this again.

I don't think will ever see a Lisp in the top 5 most popular rankings (assuming we can measure actual use, which, somewhat roughly, we can).

In 2020 we'll probably see imperative and functional imperative hybrids topping the charts.

The top languages today all existed by 2000 -- many well before that.

C: 1972

C++: 1983

Objective-C: 1983

Python: 1992

PHP: 1995

JavaScript: 1995

Ruby: 1995

Java: 1995

C#: 2000

Of course, these rankings are skewed by existing codebases, but they're still topping the charts. In 2020, the languages at the top of the charts will surely have existed in 2010 -- what imperative-functional hybrid that is presently in use did you have in mind?

I don't know what specific language GP had in mind, but I would guess it's probably one of OCaml, F# or Scala. Or Rust - did Rust development start before 2010? Or one of the Lisps, where Clojure has a bit "worse" (or better, depending on who you ask:)) imperative story than Racket (the latter has mutable lists and hashes, although they are discouraged and not the default), but both are on the way to popularity (I still believe in this) and have mixed, imperative, generic, OO (Racket) and functional features. Of course Common Lisp, which is similarly a mix of paradigms, has a chance to make a great comeback too. There's also JavaScript, especially with improvements it's going to get in the future, but this damned thing already is 9th according to TIOBE[1].

Honestly, I wouldn't be very disappointed if it proved to be like this. All these languages are interesting and in 7 years time could become true "languages of the future". However, despite hoping so, I don't believe it will come true.

Most probably some languages will just move slightly higher with others moving a bit lower. Java will remain in the first 3, at the very least. Maybe Objective-C will swap places with C++ and Python or Ruby (or both) will come close to PHP. That's all I think will change, although it would be nice to see Rust instead of C++ for example or Clojure in place of (Visual) Basic.

Also, all the currently most popular languages are this young because there was a major paradigm shift from imperative to OO in the '80. Without similar shift happening sometime soon we will be stuck with Java not just to 2020, but to 2040 at least :)

[1] http://www.tiobe.com/index.php/content/paperinfo/tpci/index....

Well, by "imperative-functional hybrid" I wasn't going for maximul functional-ness, e.g something like Ocaml or F#.

I was referering to stuff in the current trend of imperative languages with added functional capabilities (from first class functions to closures to macros) and used with a dash (or several) of functional idioms.

So stuff like C#, Scala, etc would qualify. Javascript and Ruby too. And Rust, if it hits off.

And I also had in mind how those and their libs would evolve more "functionaly" going forward to 2020.

Racket also has a typed version. It seems to be the preferred version for many of the more serious projects.

It does[1], but it's not necessarily preferred. The whole point of Typed Racket is that you can write your program in (untyped) Racket originally (maybe starting out as a small script), and then add types if you need your program to be more robust. Since typed and untyped modules interoperate smoothly, you choose whatever combination works for your project.

That said, some libraries start out typed too (e.g., the Racket math library[2]) and do benefit from type-driven optimizations and type-checking.

[1]: http://docs.racket-lang.org/ts-guide/ [2]: http://docs.racket-lang.org/math/

Typed Clojure is in fact based on the research behind Typed Racket.

Admittedly, I'm only a novice at clojure (working on it), but I find that type error really unfriendly. It doesn't provide any clearly actionable information, something like "+ expects it's 2nd argument to be Integer, but internal-number is a Number"

Some terminology might help..

The domain is the inputs of a function and the range is the output. I assume that if there were multiple arguments to the function it would git you the other ones too.

BTW compare it to the haskell version of the same:

  > 1 + "1"
  No instance for (GHC.Num.Num [GHC.Types.Char])
  arising from a use of `GHC.Num.+'
  Possible fix:
  add an instance declaration for (GHC.Num.Num [GHC.Types.Char])
The shirt looks significantly less hairy in clojure :-)

The Haskell error is made more complicated by the fact that Num is a typeclass that other types can participate in. Other type errors are more tractable:

    Prelude> 'a' : 'a'
        Couldn't match expected type `[Char]' with actual type `Char'
        In the second argument of `(:)', namely 'a'
        In the expression: 'a' : 'a'
        In an equation for `it': it = 'a' : 'a'

The reason behind that error message is how generic numbers are in Haskell by default. 1 + "1" will actually be working code if you make an instance for Num [Char].

This looks really nice.

I've been wanting to get into clojure (mainly for the web aspect of it) for a while, but as someone who hasn't done much lisp and really loves Haskell's type system I've been hanging back a bit.

I'm assuming that clojurescript will also work fine with this?

This doesn't get you anything like the expressive power of Haskell's type system, though.

If you're going to make such a claim can you explain the capabilities, tradeoffs, and limitations of Haskell's support for typed records over what Typed Clojure provides?

No, I can't.

I claimed that Typed Clojure doesn't get you the expressive power of Haskell's type system. I understand that Typed Clojure has some sophisticated facilities for refining types, and (e.g.) tracking what keys a map has, and lots of other neat stuff. This does not seem to me to increase the expressivity of the language, whereas Haskell's type system does (well, since I don't think one can separate Haskell into the language on the one hand and its type system on the other, it's misleading to talk about Haskell's type system increasing its expressivity). (I just about plotzed when I saw the definitions for `eval` and `view` in <http://okmij.org/ftp/tagless-final/course/Intro2.hs>.)

Everyone who writes a monad library for Clojure, for instance, has to take special measures to implement a properly polymorphic "return", or punts and doesn't do it. If Typed Clojure let me annotate `(return 5)` [ETA: or annotate something dynamically enclosing `(return 5)` with something that would force the given interpretation of the return call] with the type list of integer, or maybe integer, or whatever, and have it actually evaluate to [5] or #<Just 5>, or whatever, that would be great. AFAICT, it doesn't do that.

> This does not seem to me to increase the expressivity of the language, whereas Haskell's type system does

Some of us don't view that as a wanted feature.

I wrote more optional/modular/pluggable type systems before. See: https://news.ycombinator.com/item?id=6196466

> polymorphic "return", or punts and doesn't do it

Haskell's type inferencer is doing a "search" for a type that fits and slotting that in there as an implicit argument. An error occurs if two match and you need to provide a type signature to differentiate. You could do precisely the same thing using first-class type objects, which Clojure has. Haskell has finally added them too, but required a compiler change. (See: Typeable).

That's how type classes work in general: A dictionary of methods has to be threaded through, unless the compiler can prove that the dictionary is a constant. Clojure does the same thing by embedding a pointer to that data in the first-class function object.

Let's assume I have a typed module. I could trivially query that module, since the type descriptions are data. That's what the Haskell compiler is doing, but with first-class types, I can do it myself. There is no reason that a user-level macro can't add enough inference to perform polymorphic returns.

But even if I could do this, I wouldn't want to. At most, I'd want an "infer-monad" form where I can explicitly say that I'm using my types to convey intent and am willing to pay the cost of coupling my program to a particular type system.

> Some of us don't view that as a wanted feature.

No doubt! But someone who was describing his or her fondness for Haskell's type system---the person I was initially responding to---probably does.

I'm familiar (though not intimately familiar) with the idea of dictionary passing and with the fact that type classes are implemented that way. I'm not sure what the point of the remark in the present context is, though. I gather that you aren't a fan of the compiler doing this for you. (I'm not precisely sure what you mean by first-class type objects here, given the mention of Data.Typeable; concretely, I'm aware of three monad libraries that offer something like a polymorphic return and I know how two of them do it; one with symbol macros and regular old maps, and one that (in the presently released version) uses a run-monad function that threads through a regular old map. You presumably are thinking of something else; if so, I'd be glad to know what it is.)

> There is no reason that a user-level macro can't add enough inference to perform polymorphic returns.

Well, if you say so. I can't quite picture how this would work as a macro, but I'm not going to deny it can be done on that ground.

FWIW, regarding your last sentence, I find the idea of a program separate from a particular type system kind of hard to grasp.


Forgive me for being the nitpicker on this article, but, dear author: check the CSS font-family on your <pre> tags. You have some proportional fonts in there, and for me on Linux, your code snippets are showing up as proportional fonts which makes them not line up.

Specifically, I'd recommend changing

    font-family: Monaco,Bitstream Vera Sans Mono,Lucida Console,Terminal;

    font-family: Monaco,Bitstream Vera Sans Mono,Lucida Console,Terminal,monospace;
The "monospace" at the end is generic and will always produce a fixed-width font.

Great post anyway, thanks!

Fixed and pushed. Should appear on the site shortly. Thanks for the feedback. I'll send a pull request to the template author as well.

So, is this also working with Clojurescript?

I haven't tried it myself but based on the twitter chatter between @swannodette and @ambrosebs I believe so.

Is not Rich Hickey in opposition to typing systems? http://www.reddit.com/r/programming/comments/lirke/simple_ma...

Rich Hickey is generally pretty open to all different kinds of ideas and ways of thinking, which becomes apparent once you watch his talks, listen to his interviews, or read what he's written (other than tail ends to Reddit threads).



a lein plugin that allows cf-ns all namespaces without firing up repl would be awesome.

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact