Common Lisp could evolve into totally parenthesis-free language. By expanding the "powerfull and versatile" Loop-macro into full powerfullness and versatileness. Unfortunately two parenthessis needed, but you can redefine the language having those by default around every file.
It was originally supposed to, s-expressions (parenthesis) were supposed to be eventually replaced with m-expressions which were influenced by Algol and FORTRAN (the irony), but reviewers of the paper and the initial implementors of lisp preferred s-expressions, which stuck.
That being said, having worked professionally in Lisp, the “un-lispy” macros like Loop are some of the worst parts of that language. They completely violate the otherwise standard-ish syntax of Lisp, are very poorly documented, impossible to memorize, and are really hard to debug despite the otherwise excellent tooling available for top tier implementations like SBCL. It was our standard practice to replace these macros with more verbose but idiomatic lisp whenever possible.
Iterate is more lispy, and for people that don't like macros transversing the loop body (because the errors you get when something like that goes wrong are hard to debug), something like foof-loop for scheme is pretty nice. I am writing this on neanderthal GPRS internet, so I leave finding documentation for these looping facilities an exercise for the reader.
Just a jumble of random words. They are especially difficult to non-english speakers, because they are not grammatically correct sentences, but rely on some unknown word-order logic in indo-european languages. Same aplies to list comprehension sentences in Python too.
Difficult to explain cause not an expert, but most languages do not have "list comprehension" by post-description. "New York has rats bigger than a dog" comes to mind. In Finnish you can but the adjective word have same ending or grammatical case as the main word. Without those clues the sentence would be nonsensical.
> You just string words together and hope it makes sense.
I beg your pardon? Messing the word orders in French or German will definitely mess up the sentence.
The German would be something like “Im New York sind Ratten, die größter als Hunden sind”, and moving the words would make the sentence invalid – albeit probably understandable.
Is that truly a stumbling block? I ask because English is not my native language. Around the time I learned English in high school I also became interested in computers, which to me then meant programming in BASIC. It took an embarrassingly long time for me to realize that e.g. GOTO was actually formed from 'go to' -- to me, "GOTO <n>" just meant that program flow would continue at line <n>, nothing more, nothing less. "IF" was just a mnemonic introducing a conditional branch, etc. . I didn't perceive my poor grasp on English as a hindrance in learning to program (as thankfully introductory text were available in my native language). Of course, you need a good handle on the language, when communicating with co-workers (and all code is communication), but that came much later.
LOOP is just a little bit more complex than BASIC:
(LOOP FOR I FROM 0 BELOW (LENGTH S) THEREIS FOR
CH = (CHAR S I) WHEN (< I 0) ALWAYS DO (RETURN CH))
*** - loop: illegal syntax near for in
(loop for i from 0 below (length s) thereis
for ch =
(char s i) when (< i 0) always do (return ch))
That might be the case, the same approach would work here though I think. There is even less a pretention of proper English grammar and why should there be? I can't remember the exact syntax of all the LOOP terms for the life of me and generally try to keep it simple.
https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node240.html#... states that "The thereis construct takes one form" so that will be your error. Not quite sure, as I don't know what you intend to express. when (< i 0) makes no sense as i is always positive.
The problem with the Loop macro isn’t the English, it’s that it doesn’t look anything like the rest of the language. It’s like finding some embedded Python in your Java; even if you’re fine with using Python, it’s going to be pretty hard to work with when you’re in Java mode.
Also, macros are much harder to debug, especially when the macro expansion code itself crashes. The loop macro does a lot of work, which makes it a particularly annoying macro to fix when things go wrong.
The 'go to' thing probably took me 30 years to realize. I was puzzled when I read the 'go to considered harmful'and the 'structured programming with go to' article until I realized.... It is probably the same as in your case, I learned programming before learning English.
The parens made me reluctant to try lisp. Then, 5ish years ago I decided to "just do it", and now they don't bother me at all.
The experience of a beginner is also important though, especially for language adoption, this might be one of the reasons why lisp flavours are not more popular.
Clojure and F# are my two favourite languages and I have similar experience. It is much faster to develop F# code because I can rely on the type system to help me handle all the edge cases, pass in the right things and avoid nulls.
I have looked at both without any prior experience in them and OCAML tooling is horrible. Also, with no support for unicode in Ocaml, F# is just much better in this regard - even if it lacks modules, functors, etc.
This is the old view. In 2020 OCaml tooling, if I may be brave to say so, is excellent. Check out the triumvirate of dune (build), opam (external dependencies) and ocaml-lsp (IDE smarts).
P.S. ocaml-lsp uses merlin in the backend (ignore that if you are not familiar with merlin).
I just spent 2 hours on this excellent tooling to try to upgrade a package with opam, I finally gave up after read all the Github issues and Stackoverflow, etc.
I spent two evenings (3 years ago, admittedly) to get the tooling setup so that I could install libraries. I got nowhere and gave up. I'm quite interested in the language but everything else seems to be a dumpsterfire.
I just tried it again, everything (m4, dune, opam, ocaml-lsp) installed just fine; then I created a simple hello world project (Quick start guide: https://dune.readthedocs.io/en/latest/quick-start.html) and I could compile and run it. Oh, how things appear to have changed.
In my experience it isn't much worse than equivalent C# solutions, perhaps a fraction slower.
Having .NET CORE as a foundation means that whatever improvement the platform gets, F# gets.
This means that not only gets to interop with all the NET frameworks and such (good or bad), but also enjoy the massive performance improvements that's been going into CORE lately (compared to old NET).
If you're already doing C#, then it's easy to start using F#. You probably are using VS Studio/Code, you already know the standard .NET libraries. etc.. F# is a little weird because there are accommodations to ensure it will interop with C#. But it is fun.
Can someone describe to me what is good about OCaml? Unfortunately I think I've only eve heard complaints about it, many being mentioned here (multicore support, bad tooling, things like utf support you'd expect to be built in, Windows pains, etc.). I've also read a number of admittedly older peices criticizing it's type system among other things. When or why should someone consider using OCaml versus a lisp, modern mainstream languahes or even a purely functional language like Haskell?
F# has (almost) everything that is available in the CLR, which means tons of high quality libraries, ready to be used in production.
Another advantage is AWS support. Since C# is a tier one language in the cloud F# is also a tier one language. I am not sure where OCaml falls into in this space.
Other than these I am not sure why, but these are pretty big ones for us. I love OCaml and trying to use it whenever I can, which is not that much unfortunately.
You use F# because your use case requires .NET. You can’t write AutoDesk plugins in OCAML, or at least I’ve never heard of that happening. Sometimes you don’t actually have meaningful alternative programming languages for certain uses.
I really like both of these languages but don’t work in either and find that I struggle to manage these two totally different platforms for hobby work. Both communities have strong tooling norms such that I feel stuck either burning side project time getting up on tools or burning time fighting “the way” assumes by most online materials.
Any tips on how to have just a little bit of Clojure/JVM and f#/CLR in your life when day-to-day is on neither platform?
Great question. Our entire company has moved to F#/CLR/.NET Core at the beginning of 2020 and we could not be happier. It started in 2019 as a hobby project, I just wanted to create a simple (not in par in terms of features) replacement for Google Analytics to track how users use our website and also create an ETL pipeline for it using AWS Lambda. This is how I got into it more in depth. The more I worked with it I started to like it even more. When the time came to select the stack for our next project we had Rust and F# as the two options and F# won because most devs were already familiar with C# and we did not like few things about Rust (especially the string handling) while we could not find any particular issue with F#. Now, 100.000 lines of code later, it was a pretty good decision.
As far as Clojure goes, I usually use it for the JVM world, if I get a project and there are no other devs. Unfortunately it is almost impossible to convince Java devs to use Clojure and there are some limitations (Java interop for example) that makes it hard to sell. I am not even sure what would I do in your situation.
> Clojure and F# are my two favourite languages and I have similar experience
Same. I have worked in both F# and Clojure profressionally. I love them both for different reasons.
As a fun thought experiment, I have often tried to force myself to choose between them, even though one doesn't really have to make a choice. Turns out that it's a tough choice to give one up over the other.
I tend to lean slightly towards Clojure at present, but then I'm using it a great deal at work.
However, if you're looking for a modern strongly typed, immutable functional language with type inference, then F# is really excellent.
I haven't used Haskell professionally and have only played with it but my feeling is that I prefer F# to Haskell because it has some "escape hatches" built in, in order to interop with C# and other .NET languages; So no IO Monad is required. It feels like more of a supporting structure and less of a straightjacket to me.
I'm always a little frustrated whenever I see this aphorism perpetuated since I think it gives the impression that a static type system is doing more than it is actually doing. In type systems that are used outside of academia, the main thing your static types are doing is checking whether the shapes of your data and functions all line up. With some small exceptions, that's it: the compiler will tell you if your LEGOs fit together, but not if you've built a knife instead of a fork.
I know what this aphorism is getting at, which is that in some domains and circumstances a lot of your bugs are "shape" errors, and it's gratifying when you're writing OCaml or Java and you can, say, refactor without hesitation, knowing the compiler will find even the most unused paths where you need to modify an invocation of your function. But it's been my and others' experience that there are many times where this isn't your most pressing concern, and could in fact be a pretty boring concern that you don't even want to pay the type annotation tax for. (I also think there's something to the idea that a static type system can instill too much confidence and encourage the pursuit of bad abstractions like 18-arg functions that can no longer be effectively reasoned about by an unaided human mind, but that's another topic.)
Type systems can encode proofs of properties that you want your data to have. For example, suppose you want to write a function that returns the first element of a list.
head :: [a] -> a
This is the type of a function "head" that takes a list of values of some arbitrary type a, and returns an a.
if you feed this function an empty list, what will it do? You can reason from the type signature that the only thing it possibly can do is crash, since there's no way to produce a value of type a without knowing what a is in advance.
head (h:_rest) = h
head [] = error "Our function is badly behaved!"
To ensure that our function never crashes, we can encode the fact that our list is nonempty in a datatype.
data Nonempty a = Nonempty a [a]
Ie, a nonempty list must contain a value of type a, and a (possible empty) list of as. Then instead of writing a function that crashes when it receives a nonempty list, we can write a function on nonempty lists that never crashes
head :: Nonempty a -> a
head (Nonempty x _rest) = x
What I'm getting at is that, yes, this is just the "shape" of our data. But you can encode more interesting and valuable properties in that shape than might be apparent at first. There are many other interesting examples of using types to ensure that properties that we want to hold for our values, do in fact hold.
Edit: I should probably add that "If it compiles it works" is absolutely not always true. It's more of a community in-joke than an actual belief. But it does turn out to be true surprisingly frequently.
In Haskell, it's both common and idiomatic. There's always a tradeoff between the usefulness of having static guarantees, and the complexity of the type system features that enable those guarantees. In some cases it's more trouble than it's worth to enforce certain static properties - you have to weigh the costs and benefits on a case by case basis. But it's nice to have the option, and Haskellers make use of it frequently.
You need a type system that is expressive enough to allow you to encode logic in the types. Of course, Haskell is not only language with such a type system: PureScript, F#, Rust, OCaml, Scala have similar features, although not all.
How many functions have you seen, in Haskell, that take Nonempty a versus taking [a]? There are reasons why this type of pattern doesn't scale well, and the Haskell designers knew it. Similarly, if they had defined head [a] -> Optional a, that would have produced complications of its own.
The general consensus in the community afaik, is that
head :: [a] -> a
in Prelude was a mistake, and the default should be to return a Maybe/Optional. Beginners are generally advised to avoid head, and all other partial functions in the Prelude, such as tail and the indexing operator, wherever possible, and rely on pattern matching instead, which is generally sufficient and more idiomatic for the relevant use cases. I agree that having the option to use a partial function is useful, escape hatches are important, but it shouldn't be the go to.
Nonempty was just a simple example. You're right that it's not very commonly used (probably because it's not in Prelude), but the general culture of writing type safe interfaces and pushing guarantees to compile time %100 is a thing. A commonly used, more complicated example might be Servant, which is a web library which statically ensures your implementation meets an API spec.
Well, since the Prelude was never changed to at least add an alternative headMaybe, perhaps it was not seen as a deep enough mistake.
Yes, I am aware that the Haskell community generally tries to push more guarantees to compile time, and that Haskell's ease of defining and using new types equivalent to existing ones but that signify some property helps a lot in this.
Still, there is some limit to this. For example, you have many libraries adding measurement units for Haskell, and many libraries doing linear algebra. But you won't find too many people doing linear algebra with measurement units, because the types of intermediate results explode too much (e.g. a type-safe unit-of-measurement matrix multiplication of 3x3 matrices has 18 type parameters, with complicated restrictions between them).
We've been using this approach in Typescript. It's more palatable than Haskell since you can express an array that has at least one element as a variadic tuple `[T, ... T]`. Having the possibility to encode invariants in types is very useful, although it gets harder at times since TS chokes when you push it to its limits. Note that this happens in an old Meteor codebase that was completely JS a year and a half ago.
I find these generally worth it because you can surface those aspects up to the function signature. It would be a lot nicer to have a function signature that shows the limits than it is to have a failure and have to open the docs for the library to see what the limits are.
If the code is sound, it should have the same complexity anyways. I.e. putting a non-empty requirement in the function signature is just as complex as adequately handling an empty array in the code.
My personal struggle is that statically typed languages are annoying to prototype (I don't care if it segfaults on my laptop, I want to validate my idea). My struggle is prototyping in something that I can easily transition to a strictly typed version. I'm curious to try prototyping in Javascript and adding types when I move to production.
I used to think Javascript was a great prototyping language. In fact I specifically practiced prototyping with it -- my github is littered with repo's for each of my prototypes -- I call them the Laundromat Demos. I would use the 70-80 minutes I had at the laundromat to write a prototype for a game in JS.
However I've tried something similar with Haskell. I think I prefer Haskell for prototyping these days. It turns out you can get a long way playing with the types. Often I spend more time prototyping the types and they guide me to the implementation which is often quite small.
It takes some training and work to get there but I think Haskell, for me, is a better prototyping language -- I don't have to deal with undefined, accidental prototype overloading, etc.
+1, I'm impressed how far these prototypes can get you, especially if there's an extra layer of correctness on top of the existing Haskell type signatures. For instance, LiquidHaskell [1] can naturally shift you towards implementing most of the necessary validation and parsing logic as part of your work on precise type signatures.
For the curious, I suggest reading Philp Wadler's "Theorems for free!" which explains how to derive a theorem for a given type. Practically, this helps compilers make certain optimizations based on the theorems it derives from the types.
In a better world it would. Currently it the head of an empty list crashes the program. Many people in the Haskell world are really salty about this but they don't want to break the API for it.
It works two ways though, which I think is important. Yes, type systems check the shape of your data which is nice.
However, when sufficiently experienced, the type system also guides the developer into good program design. Type systems for me are more a design tool than shape checker.
The checker/inferencer helps me learn about assumptions I make about data in specific parts of my code, which allows me to remodel my data types to better fit my intent. This works cyclic until things just seem to ‘fit’.
Personally at least, this allows me to write far more complicated applications than I would ever be able to do without types. And it’s just very hard for me to imagine this wouldn’t be the case for any other developer.
> I'm always a little frustrated whenever I see this aphorism perpetuated since I think it gives the impression that a static type system is doing more than it is actually doing. In type systems that are used outside of academia, the main thing your static types are doing is checking whether the shapes of your data and functions all line up. With some small exceptions, that's it: the compiler will tell you if your LEGOs fit together, but not if you've built a knife instead of a fork.
Disagree. You can and should be using your type system to express your system design. If it's important that your utensil has at least 3 prongs, it should only be constructible by passing at least 3 prongs; that much is trivial to do even in Java, yet alone a language with a decent (i.e. ML-family) typesystem.
I frequently have to defend why I hate Python but the foremost reason is that lack of static typing (which also makes reading other peoples codes harder than it need be).
Static typing, especially in a rich type system like, e.g. Haskell and Rust, doesn't guarantee you don't have bugs, but it almost guarantees the ones that remain are better bugs, that is, application level bugs. However in my experience, I can usually tell what kind of code will be correct once it passes and which might still have issues.
Absolutely. The more experience I have, the more repugnant dynamically typed languages seem to me (for domains where they fit, i.e. I'm not talking about untyped assembly or whatever).
Which is absolutely bizarre to me because most of my experience with Python has been with people who are effectively scientists who don't give a rats-ass about good coding practices and yet I rarely had issues with receiving the wrong type.
Don't get me wrong though, I spent the first 10 years of programming with C and Java so I understand the comfort that a statically typed language provides. I just haven't found it to be a big enough issue over the past 7-8 years of programming in Python to really complain about it.
Frankly, if you were to ask me what the top 3 sources of my headaches in Python are, number 1, 2, and 3 would be, receiving a None value when I expected something else; but that's an issue with most languages.
> Frankly, if you were to ask me what the top 3 sources of my headaches in Python are, number 1, 2, and 3 would be, receiving a None value when I expected something else
Curious example, because that's absolutely something a sufficiently advanced and well-thought-out static type system can completely solve.
You're right that the type systems of C and Java do not meet this bar, however.
Is null even a type though? If not, then it would seem it's not a static type system that's solving it; it's something else entirely solving it (whether or not that something is contained in that specific type system).
In Common Lisp, T is the "any" type (which makes sense there, as type specifiers can be formed using unions using (or type-A type-B ...) lists), the value NIL is the sole instance of the type NULL.
Receiving a None or Nothing or a NULL or similar is something a type system can paper over or, more politely, force you to add code to ward off but there's something happening in the actual data, if not the real world, which is causing that non-value to be present and the type system can't fix that.
But is the issue receiving a null or is it how unexpected the null is? Assuming that functions always can return null is way too tedious, so in practice my issue with dynamic languages like Python is that I'm never sure whether a function can actually return null.
> you don't even want to pay the type annotation tax for.
95% of annotations in the average haskell codebase are unneeded for compilation. People still write them, because they are a useful source of information when reading the code.
So tell me, if you feel like you don't want to write down that one line of comment which happens to be checked against the compiler, I guess you probably don't write tests or documentation either?
> I guess you probably don't write tests or documentation either?
Why would any experienced programmer make such a blanket statement? As always, it depends.
Sometimes there is value in documentation (or whatever unchecked types in fancy languages), especially for stuff that is read a lot more, by other people, or yourself in the future.
For a lot of stuff there is no point in writing documentation. For example, if the code is obvious and pedestrian. Or if you'll just throw away everything, or refactor a month later.
And very often, writing documentation is not only a waste of time, but downright harmful, because code and documentation easily diverge.
My personal advice (not just for documentation) is to strive on the side of doing as little as you can get away with, and only introduce structure and bureaucracy if there's a clear indication that it's required.
The fact that code and types don't diverge can make types useful as documentation, albeit limited documentation. Ideally the compiler figures out the types so you don't have to. Haskellers often find this useful when they refactor a month later.
Code and types can very much diverge if the types aren't checked by the compiler, as my parent poster indicated.
Case in point: type aliases instead of (Haskell) newtypes.
Type aliases (which are unchecked, unlike newtypes) can lead to exactly the kind of diverging that I alluded to. Look at the mess that is all the typedefs and defines in the Win32 API for example. It's extremely hard not to pass in the wrong typedefs, and there must be extremely few programmers who know which aliases are the same on every platform and which could change.
Aside, newtypes (Or the equivalent in C for example, wrapping a single member in a struct) can be very annoying. I would advicse against this, just like I'd advise against using type aliases.
> which happens to be checked against the compiler
as:
> the types aren't checked by the compiler, as my parent poster indicated.
(also type aliases are still checked. The check is less useful, but it's still a check)
> Why would any experienced programmer make such a blanket statement?
Because type annotations are less work than tests or documentation, don't suffer from falling out of sync like doc does, and because experience shows that haskell users typically find it useful to write types despite it not being necessary to compile. It would be surprising to skip types but invest in writing more complicated means of documenting and testing the software.
What I mean is that these annotations are going to prevent compilation if you get them wrong, but the compiler's type inference is good enough that you only need to provide types when there is ambiguity.
That means most of the type annotations written exist because developers find them useful for documentation.
If that change goes through your whole program without causing any breakage, from Haskell's point of view it's a good change.
If you have an API that exposes data to external systems, that may cause an integration breakage, indeed. That is why Haskell users like to use libs like Servant, which let you specify your API as a type. This way, if you change your system, your API has to change aswell.
Another benefit of Servant is that you can generate swagger and clients for other languages easily, which your users can use in their own build to get type safety too.
Agreed. I have a strong background in 'bad' programming languages like Go, Python, C. Half a year ago I had a short stint at a Haskell shop and was thoroughly disillusioned.
That particular 'writing against the compiler' approach lended itself to write-once, over complex type juggling code. Because why make it readable - if it compiles, it works, and if it works, there won't be any need to read it, right? Well, except you still get bugs, and then debugging anything is like making sense of brainfuck. Maybe smarter people than me can juggle a handful of types and operators simultaneously in their head - I can't.
My favourite example of 'just because it compiles it doesn't mean it works is' Maybe [a]. I've had a colleague scratch their head for hours over something like this:
userCount :: Foo -> Int
userCount f = length $ getUsers f
This compiled, but always returned 1. Why? Well, because turns out `getUsers` returned `Maybe [User]`, and not `[User]` as the programmer expected, and somewhat asserted in their mind with types. And it also turns out that in Haskell, `length` takes a `Foldable`, and `Maybe` is a `Foldable`. Thus, a `length` of a Maybe will gladly return 0 or 1, which makes perfect sense from the point of view of the type system, but zero sense when it comes to humans. Whoops.
I'm not sure what "Haskell developers" are saying, but this Haskell developer says "Haskell provides many tools that can be used to solve programming woes. One such tool is its strong and flexible types system (there are several other tools, such as immutability, functional style)."
And Python provides readable syntax. Your example has an easy-to-spot bug, just from reading the code and knowing Python's semantics. Haskell's "programmable semicolon" ends up being a problem here because, thanks to typeclasses, the programmer usually does not know which semicolon they are programming under!
"Python provides many tools that can be used to solve programming woes. One such tool is its strong and readable syntax." Do you see how empty this sort of platitude is?
I don't think that illustrates that promotion of the type system as a useful tool is an "empty platitude". A well constructed syntax is certainly a nice feature of a programming language. One might say a language like brainfuck does not have a syntax which makes the code easily readable, and therefore is the worse for it.
Similarly, one might say that a strong type system is a useful tool. That doesn't make it a panacea (and I don't think that this was the claim), but that doesn't mean one can't appreciate it as a good tool.
The problem with this comparison is that readable syntax is a subjective notion, whereas type system is an objective thing that can be evaluated based on its strictly defined boundaries - it's either possible or impossible to encode all required software specification with a provided type system, and if something is not possible, we know exactly what are these things that don't fit into the type system, and therefore we cannot rely on the compiler to prove their correctness.
Er, the difference here is that we're given the Python code for `getUsers` but not the Haskell code for it. That makes the error in Python obvious. The incorrect `userCount` in Python looks just as obviously correct as the incorrect `userCount` in Haskell; and conversely, if we were given the code for the Haskell `getUsers` (or even just the type signature!) the error would be just as easy to spot.
That doesn't really "fix it". Even assuming there are no other callers of `get_users` that you might be breaking here (possibly silently, since lists and tuples in Python have overlapping interfaces), you're making `get_users` less useful by dropping structure that at least someone, at some point, thought was useful.
Also, you should annotate the type of your lists :)
Why does getUsers return Maybe [a] in the first place? It should be [a]. You can also implement your own version of length that works only with iterables of your choice. The language allows leveraging type constraints to achieve this, the fact that your team didn't use it doesn't indicate an inherent flaw in the type system you've been provided with. And if you want to go an extra mile, use liquid-base instead of base.
I know, not what you meant. But a custom sum type would not only allow you to return multiple different failure cases, you also wouldn't give it a `Foldable` instance with surprising semantics.
Plain old data types are seriously underrated these days.
Right, so we essentially say that the provided example doesn't refute the "it works if it compiles" slogan. It just works according to the provided specification expressed in the types, and it so happened that the specification was not specific enough.
My specification doesn't make a distinction between absent users and possible error responses because I assume that errors will be processed elsewhere and the getUsers function is applied as part of the "safe core" interface that is not concerned about error handling. If the specification was more precise (even more precise than in your suggestion, for instance, getUsers :: SafeIterable a => Foo -> a and length :: SafeIterable a => a -> Integer ), it would work differently.
The issue in the example has to do with the clarity of the intent encoded in the type signature, not the inherent issue of the type system that cannot guarantee safe runtime of the example. That's why I mentioned type constraints for specific iterables as a possible solution to the encoding problem.
A better example of the compiled-but-not-working case would be something that currently cannot be expressed well by the type system, something like async exceptions or access to closed resources (will be covered by linear types soon).
I find explicit null checking almost as valuable as static typing. Tests can cover a lot of the same ground as types but it’s very easy to have tests that don’t anticipate the nulls you only get in production.
> I think it gives the impression that a static type system is doing more than it is actually doing
This statement probably gets to the heart of the issue.
A static type system isn't something that just "does things", nor can we conduct a proper analysis when the dialectic is reduced to "static vs dynamic".
The first thing to realize is that there exists a spectrum in how much we encode in our type system. Even in weaker type systems like those of Go or Java, we can make illegal states unrepresentable, encode invariants, create typesafe abstractions, etc. Or we could just treat everything as object and not make any attempt to encode anything at all in the type system.
In the former case, the aphorism "if your code compiles, it probably works", is more true than in the latter case.
The next thing to realize is that the expressiveness of a type system places a ceiling on what can be usefully encoded in that type system. So in languages like Haskell, it's possible to get closer to the "if your code compiles, it probably works" ideal, than in a language with a weaker type system. But even in Haskell, it's possible to just write everything in IO and not make any attempt to encode your constraints in the type system.
So in that sense, a type system is just a tool. If wielded effectively, it lets us offload a lot of work to the compiler in a sort of symbiotic back-and-forth. And some languages give us better static typing tools, to make that symbiosis more effective. But if we're not using simpler type systems to their fullest, we're not likely to be pushing against their limits, making it a lot harder to justify more advanced type systems.
So "if your code compiles, it probably works" shouldn't be read as a statement of fact, but as an aspirational goal, one that we can move towards by more effective use of our tools.
> the compiler will tell you if your LEGOs fit together, but not if you've built a knife instead of a fork.
OK, let's stretch that metaphor a bit. A powerful type system like Haskell's is really a way of both restricting which LEGO(tm) pieces you can use and designing custom new ones. So when you design and write software in truly idiomatic Haskell, after getting all your types lined up there is no way to make anything other than a fork.
Not just shape of data, but the values that are allowed by data.
I agree that it is an oversold fallacy and types get really clunky when pushed too far. But any worthy type system prevents lot of errors. This is not generally acclaimed by empirical studies claiming only 5% improvement because they are caught at compile time.
Other benefits are enough to use a sufficiently good static type system, by the way, even for people that think type systems aren't better than TDD.
* Better autocomplete
* Better error highlighting - no need to run code or tests, IDE can highlight instantly.
* No typos in variable names (can be achieved in some but not all dynamic languages)
* Performance of compiled code
> static types are ... checking whether the shapes of your data and functions all line up
They definitely do more than that in Haskell. You can define distinct types with the same "shape", and the `newtype` keyword specifically creates a new type that has the same "shape" as an existing one, but is nominally distinct.
This nominal typing, as it's called, allow you to express intent in types. Type checking then doesn't just check whether "shapes" line up, but also whether your intentions line up. If you express contradictory intentions, you probably made a mistake.
Practically speaking, if the only types ever used in a Haskell program are `Int`, `Float`, `Char` and nested lists of those, all the type checker can ever do is tell me when I miscount square brackets or forget a `fromIntegral`. Those programs never run on the first attempt.
Yes, I'm somewhat puzzled that static typing seems so important to some, while it clearly only prevents one class of errors (and depending on the language, even that only to some extend, e.g. it's still possible to initialize enumeration class instances with invalid values in C++). I'd have expected that Eiffel's pre- and post-condition or design by contract in general, would have gained more traction with those who care about eliminating possible sources of programmer errors.
> you don't even want to pay the type annotation tax for.
You won't understand those aphorisms as long as think of "type annotation" as something useless, "tax-like". I know where you come from, in languages like Java it is a tax - you repeatedly write long-winded types in different places.
With type inference, that changes. You have a REPL (or IDE integration) that you can just ask for the inferred type as soon as you write a function. Then you look at it, you think about if it expresses what you want the function to do (that already catches some errors), you copy-and-paste it, make it a bit prettier, and bam, type annotation is done. It's even on an extra line, you don't have to attach it to arguments.
Or, you design with types ("test first style", think of the type annotation as a test). You first write down the type, then you write the function, and you see if the type checker agrees. If it doesn't, you probably found a bug. Or your idea of what the function should do was wrong (has also happened to me).
And everyone who uses that system does know that type checks are very dumb checks. They only help against dumb errors. But the majority of bugs are dumb bugs (at least the majority of my bugs) - misspellings, you forget to write out some part, etc. Those get caught. The hard bugs won't - that's what tests are for. (And automated testing is again simpler in the presence of types, see Quickcheck).
> and encourage the pursuit of bad abstractions like 18-arg functions
It never does. 18-arg functions are just unwieldy. If your function grows to that point, refactor (or rather, refactor long before that). And again, the type systems helps with refactoring.
(Yes, I know I won't convince you, this debate has been going on for at least 30 years. OTOH, it doesn't hurt to see the opinion on the other side).
Every once in a while, there's a post on front page HN about Haskell and/or Lisp. Sometimes these posts get a lot of traction, but what confuses me is despite the apparent popularity of these languages among developers, still they are seldom used in serious software. I know there are exceptions (esp. with regards to Lisp), but still these languages never come close to other languages such as Java, JS, C, or even Scala.
Combination of somewhat high barrier to entry (they're unlike most other languages so experience isn't as transferable), and network effects - those in charge of choosing a technology for a project generally make the subjectively prudent decision to use technology that is already in common use, leaving the less common languages in a catch 22.
I doubt this explanation really captures the gist of the phenomenon. Many current and upcoming trends (containers/Kubernetes, machine learning and Rust, for instance) have suffered in varying degrees from either of those problems, yet their adoption among companies is ever-increasing. I suspect they suffer from an issue also shared by the likes of cryptocurrencies: they don't have a "killer benefit"; a concise, persuasive reason that using them is a discrete improvement over current practice (like "high-level systems programming!", or "self-driving cars!").
This is mostly because the majority of software developers only know Java and are averse to learning new languages. Also, companies chose Java at one point and are now taking next to no risks by introducing new languages (not even languages on the JVM). They can also just fire and rehire Java developers because there is a large pool of available developers that they can just plug-n-play, so to speak. They're not interested training people.
As a paid user of haskell and Java, here are a few of the things I experienced when discussing about language choices in corporate settings:
* Fail a project in Java, and you have a project failure. Fail a project in Haskell, and you have a Haskell failure.
* Management has a large veto power, and management usually favors technologies they are used to, or used to use themselves.
* Management also favors their ease of work with the tech. In practice, they often hold the belief that it's much easier to find java developers than it is to find haskell developers.
* Haskell suffers from a reputation of being "academic" and "hard to do real things with".
In practice, I've done great commercial projects in Haskell, and the technology was worth using. When done right, recruiting for haskell projects was much faster than recruiting for any mainstream language. On the flipside, it was sometimes hard to get adequate corporate stewardship for the language (for instance, managers/HR insisting they found no one for the job after publishing through the usual channels when you know half a dozen people at the local meetup that would jump ship if offered a position).
That has been discussed to death already. Common Lisp might be a niche, but it appears to be even more of a niche than it actually is when cursory looking at major projects. Common Lisp (and similar languages) are (particularly, not only) well suited for prototypes created by a small group of well interacting developers. If you shoot for very large systems, with a very large staff of various talent and experience (and inevitable communication break-downs), then e.g. Java is the safer bet. For this reason, rewriting a Common Lisp program into Java is not a sign of failure of the language, but a sign of success of the project.
Old and recent companies using Lisp: https://common-lisp.net/lisp-companies Includes Google (ITA Software, still powering airfare search engines), Boeing, Nasa (remember that Lisp was sent to space), Grammarly, OpusModus, Rigetti (Quantum computing), SISCOG (running metropilatan in european capitals), Ravenpack (big data analysis for financial services), PostgreSQL (pgloader), the ACL2 internationally-used theorem prover, etc
You have to remember that HN users aren't a representative subset of software developers world-wide.
Part of that may be the continuing influence of PG: I both learnt Lisp and found HN because of his essays, and I suspect I'm not the only one.
But also, the HN community generally has an academic fondness for new and niche languages, so a language's popularity on HN is a poor proxy for its popularity in the business world.
(A last point: I think the diminished importance of Lisp in the last decades is less due to an inherent flaw of the language and more to what you might call "accidents of history" - particularly its close association with early attempts at AI.)
I played around with Hackett (a parenthesised Haskell) quite a lot, and I really enjoyed creating my own syntactic abstractions in a haskell-like language.
I find I would rarely reach for macros in Haskell, but that I sometimes would like to. Lisp macros do overlap with what you can achieve with Haskell things like typeclasses, laziness, monads and template Haskell, but the overlap is far from 100%.
Just tried the code in SBCL and it definitely gives a compiler warning even without executing the function:
This is SBCL 1.5.6, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
* (defun add-text-padding (str &key padding newline)
"Add padding to text STR. Every line except for the first one, will be
prefixed with PADDING spaces. If NEWLINE is non-NIL, newline character will
be prepended to the text making it start on the next line with padding
applied to every single line."
(let ((str (if newline
(concatenate 'string (string #\Newline) str)
str)))
(with-output-to-string (s)
(map 'string
(lambda (x)
(princ x s)
(when (char= x #\Newline)
(dotimes (i padding)
(princ #\Space s))))
str))))
; in: DEFUN ADD-TEXT-PADDING
; (MAP 'STRING
; (LAMBDA (X)
; (PRINC X S)
; (WHEN (CHAR= X #\Newline) (DOTIMES (I PADDING) (PRINC #\ S))))
; STR)
;
; caught WARNING:
; The function (LAMBDA (X) :IN ADD-TEXT-PADDING) called by MAP returns NULL but CHARACTER is expected
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
ADD-TEXT-PADDING
Note that this is on SBCL 1.5.6 which is a little bit over a year old at this point; the article is from 2015 so we should pick a properly dated version from https://sourceforge.net/projects/sbcl/files/sbcl/ and check there.
The Common Lisp implementations and compilers keep on getting better with time and they produce more and more compile-time warnings (especially SBCL).
I once read modification of Greenspun's Tenth Rule which looked like "typical Common Lisp implementation contains slow ad hoc bug ridden implementation of half of Haskell type system".
That's a common phenomenon. Statically typed programming languages try to implement features of dynamically typed programming languages, whereas dynamically typed programming languages try to implement features of statically typed programming languages. On one side, we have a struggle of languages like C++ attempting to relax their own rigidness to achieve dynamic behavior, and on the other, we have languages like Javascript and Lisp attempting to constraint their own dynamic nature for benefits that come from static typing.
I haven't tried with a five year old version of SBCL, but even early on (from the beginning of the project?) SBCL treated type declarations (using DECLARE) as assertions (as opposed to mere hints for optimization), making it what some might call a gradual-typed system. So with properly declared type, SBCL could have found it then too.
Now, some discourage the use of type declarations in Common Lisp (for a variety of reasons, one being the limited portability as less clever compilers don't warn against mismatch, but might produce broken code) and suggest the (run-time type testing) CHECK-TYPE (standard) macro instead. SBCL's type inference (at least in recent versions) is sufficiently clever to detect mismatches at compile time, when the type is (implicitly) announced using CHECK-TYPE.
Note: pseudocode typed straight into the comment box, and I dropped the newline feature, but hopefully a bit more illustrative of what I'd consider 'normal' lisp.
Well, you asked for it, so: the #t gives away that you're more comfortable with Scheme than with Lisp, I suppose those string functions are present in some Scheme dialect as well, but they sure aren't in Common Lisp (not with that syntax at least). Scheme is quite dogmatically a functional programming language, Common Lisp is decidedly a multi-paradigm language. With that it shouldn't surprise that the article's author chose to write into a string in an imperative programming style. I don't think it would raise eyebrows in the Common Lisp community. There's more than one way to do it.
The string functions were basically invented to allow me to express the algorithm, though a friend of mine who writes clojure looked at the original problem and produced an implementation that was logically similar to mine that used actual clojure functions that behave basically like that.
I tend to do single assignment and minimal mutations in any language though, and try and push my side effects (I'm aware the string write is encapsulated therefore not a side effect) as far out to the edge as possible.
As an example, in perl (which is probably my most-used language, so I'm 100% on board with "more than one way to do it" ;) I'd've written:
sub add_text_padding ($str, $padlevel) {
my ($first, @rest) = split /\n/, $str;
my $pad = ' ' x $padlevel;
join "\n", $first, map $pad.$_, @rest;
}
(my lisping has indeed been significantly in scheme, plus dabbling in other dialects and a tendency to prototype things in operative lisps derived from Shutt's kernel ... which is why I did, indeed, ask for it, so thank you for the reply)
In Clojure it's common to develop your application by building it inside a running system
Every REPL form sent recompiles the running program, I don't want that fast loop to be force lagged by the program checking types
I'm more than happy for that to happen at edit time, independent of compile time outside of my application process
So I heavily encourage using clj-kondo to check for mistakes doing primitive type checking etc
To reiterate, I love static analysis but I also need control over how and when it runs, I like it in CI and during editing
Otherwise it comprises one of the best things about those old languages, (Smalltalk lisp etc) being able to dev your application by injecting code and recompiling while the app is still running
The feedback loop while coding a live image is hard to explain but anything that makes it slower is disastrous for me, I can lose focus so quick
When deving Clojure idiomatically partial recompile happens at a much greater frequency than most languages, compile time is very important
That sounds super cool, and I'd quite like to give it a go.
I'm not sure if what Haskell has is quite the same, abilities but you can hot-reload modules at runtime in GHCi, and FWIW, I've always found the type checker to be pretty zippy.
I hear this "code as system is running" a lot. Doesn't it complicate our reasoning about the program as compared to simple text files? I haven't heard the other side of this claim.
You still have the simple text files, but while you're developing (and perhaps in exceptional circumstances while it's running) you interact with it in a live fashion. But the source code is still there, and the whole thing can be recreated from scratch if you kill the image by loading the code.
This lets you do things like (stupid trivial example follows, Common Lisp not Clojure) write a function from inside out in the REPL, then move the code to source:
> (+ 1 2)
3
> (let ((x 1))
(+ x 2))
3
(defun add-two (x) ;; in a source file
(+ x 2))
> (add-two 1) ;; after evaluating/compiling the above
3
Imagine that these were more complicated things, like you're using a web API and getting JSON files and parsing them. You can develop it interactively, and as you verify that things are correct, you can migrate them to functions (and add tests to really do the verification part).
This is not the most impressive piece. Two of the main complaints about "Lisp" are that it's not really a functional language and that there aren't enough libraries/the standard library isn't what it should be. These are specific to Common Lisp. Clojure gives you a functional-first Lisp, access to all Java libraries, and a decent standard library. The Common Lisp standard is very old, even if you look at the date it was finalized (1994), but most components are from a much earlier period than 1994.
The state of the Common Lisp library ecosystem is probably the saddest thing about the language.
As the author says, the inbuilt standard library is hopelessly too small for modern requirements - even basics like string manipulation are only cursorily covered. And most of the third-party libraries out there are single-person projects, out of date, undocumented, or all three...
The language itself is still fantastic and, IMO, still superior to most other languages out there. But the lack of decent libraries is a major productivity killer.
Have another look at https://github.com/CodyReichert/awesome-cl maybe? There might be more than you think. I know I have regularly discovered great libraries the last years.
I was thinking about mentioning that list, actually. Yes, there are libraries around for most common tasks. However, there are three things I'm not satisfied with:
1) I expect common tasks to be covered by the standard library. Third-party libraries are for specialised tasks.
2) There's often three or four mediocre libraries for the a given task instead of one really good one. Of course there are some "default libraries" (alexandria, bordeaux-threads, etc.) but too often you basically need to pick one at random and hope for the best. Best example: GUI libraries.
3) Most libraries are side-projects by individual developers or only have small teams. Which means: poor documentation and (in some cases) frequent breaking changes or random interruptions in development. The lack of documentation is not quite as bad as it sounds, because the source code tends to be highly readable. But still, the lack of polish and especially stability is not exactly trust-inspiring.
The CL ANSI standard really needs an overhaul to significantly expand the number of inbuilt functions. The fact that my 1990 edition of CLtL2 still adequately describes any modern CL implementation is a testament to the thoroughness of its language design, but also to the lack of change and expansion in the 30 years since.
Alternately, the CL community should develop its own semi-official stdlib - like the R community did with the tidyverse. A single package that you can install via Quicklisp that pulls in a curated list of high-quality libraries, ready for use. The CL standard might not come "batteries included" any time soon, but a "battery expansion pack" would still be a major improvement over "bring your own batteries" ;-)
I agree with the point on CL libraries, which is truly sad after this many years. But Lisps like Clojure address this formidably by integrating deeply with the JVM and actually also Python.
Im not sure his code is idiomatic SBCL but it could be written more succintly and functionally by most LISPers.
To the point on static/dynamic typing, I disagree. But its a discussion thats been going on for so long that Ive concluded its mostly a matter of taste. For Clojurians who like static typing, Spec packs a big punch without removing flexibility.
Still, Haskell is pretty great but Im more efficient in Clojure, Your mileage will vary.
A short add-padding (missing the `newline` parameter):
;; (ql:quickload "str")
(defun add-padding (txt &key padding)
(with-output-to-string (s)
(loop for line in (str:lines txt)
do (format s "~a~a~&" (str:repeat padding " ") line))))
172 comments
[ 5.4 ms ] story [ 227 ms ] thread2018 https://news.ycombinator.com/item?id=17533341
That being said, having worked professionally in Lisp, the “un-lispy” macros like Loop are some of the worst parts of that language. They completely violate the otherwise standard-ish syntax of Lisp, are very poorly documented, impossible to memorize, and are really hard to debug despite the otherwise excellent tooling available for top tier implementations like SBCL. It was our standard practice to replace these macros with more verbose but idiomatic lisp whenever possible.
« New-York a des rats plus gros que des chiens »
I beg your pardon? Messing the word orders in French or German will definitely mess up the sentence.
The German would be something like “Im New York sind Ratten, die größter als Hunden sind”, and moving the words would make the sentence invalid – albeit probably understandable.
https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node240.html#... states that "The thereis construct takes one form" so that will be your error. Not quite sure, as I don't know what you intend to express. when (< i 0) makes no sense as i is always positive.
http://www.gigamonkeys.com/book/loop-for-black-belts.html might be of help.
Also, macros are much harder to debug, especially when the macro expansion code itself crashes. The loop macro does a lot of work, which makes it a particularly annoying macro to fix when things go wrong.
I think my hate relationship toward algol syntax was so strong, sexp were an obvious answer from minute 0.
The experience of a beginner is also important though, especially for language adoption, this might be one of the reasons why lisp flavours are not more popular.
https://aphyr.com/posts/353-rewriting-the-technical-intervie...
I always wonder: why use F# when you can use OCaml? OCaml has pretty decent third party libraries...
(Yes multicore support in F# would be a good reason but apart from that)
I have looked at both without any prior experience in them and OCAML tooling is horrible. Also, with no support for unicode in Ocaml, F# is just much better in this regard - even if it lacks modules, functors, etc.
This is the old view. In 2020 OCaml tooling, if I may be brave to say so, is excellent. Check out the triumvirate of dune (build), opam (external dependencies) and ocaml-lsp (IDE smarts).
P.S. ocaml-lsp uses merlin in the backend (ignore that if you are not familiar with merlin).
For utf8 regexes, you can use Pcre in ocaml, which is faster than camomile regexes.
Having .NET CORE as a foundation means that whatever improvement the platform gets, F# gets.
This means that not only gets to interop with all the NET frameworks and such (good or bad), but also enjoy the massive performance improvements that's been going into CORE lately (compared to old NET).
Define slow. I never had an issues with the compilation speed of F#.
> why use F# when you can use OCaml?
Even though both of them are in the same family they could not be further away from each other. OCaml has many standard libraries more you can read here: https://discuss.ocaml.org/t/what-is-the-preferable-solution-...
F# has (almost) everything that is available in the CLR, which means tons of high quality libraries, ready to be used in production.
Another advantage is AWS support. Since C# is a tier one language in the cloud F# is also a tier one language. I am not sure where OCaml falls into in this space.
Other than these I am not sure why, but these are pretty big ones for us. I love OCaml and trying to use it whenever I can, which is not that much unfortunately.
Any tips on how to have just a little bit of Clojure/JVM and f#/CLR in your life when day-to-day is on neither platform?
Essentially you can use it in most places you'd currently use bash scripts the install footprint is small and the startup time is fast
It's kind of like Clojure without the JVM, very easy to justify for one off bits and pieces
As far as Clojure goes, I usually use it for the JVM world, if I get a project and there are no other devs. Unfortunately it is almost impossible to convince Java devs to use Clojure and there are some limitations (Java interop for example) that makes it hard to sell. I am not even sure what would I do in your situation.
Same. I have worked in both F# and Clojure profressionally. I love them both for different reasons.
As a fun thought experiment, I have often tried to force myself to choose between them, even though one doesn't really have to make a choice. Turns out that it's a tough choice to give one up over the other.
I tend to lean slightly towards Clojure at present, but then I'm using it a great deal at work.
However, if you're looking for a modern strongly typed, immutable functional language with type inference, then F# is really excellent.
I haven't used Haskell professionally and have only played with it but my feeling is that I prefer F# to Haskell because it has some "escape hatches" built in, in order to interop with C# and other .NET languages; So no IO Monad is required. It feels like more of a supporting structure and less of a straightjacket to me.
I'm always a little frustrated whenever I see this aphorism perpetuated since I think it gives the impression that a static type system is doing more than it is actually doing. In type systems that are used outside of academia, the main thing your static types are doing is checking whether the shapes of your data and functions all line up. With some small exceptions, that's it: the compiler will tell you if your LEGOs fit together, but not if you've built a knife instead of a fork.
I know what this aphorism is getting at, which is that in some domains and circumstances a lot of your bugs are "shape" errors, and it's gratifying when you're writing OCaml or Java and you can, say, refactor without hesitation, knowing the compiler will find even the most unused paths where you need to modify an invocation of your function. But it's been my and others' experience that there are many times where this isn't your most pressing concern, and could in fact be a pretty boring concern that you don't even want to pay the type annotation tax for. (I also think there's something to the idea that a static type system can instill too much confidence and encourage the pursuit of bad abstractions like 18-arg functions that can no longer be effectively reasoned about by an unaided human mind, but that's another topic.)
Moreover, in OCaml it is idiomatic to use type inference everywhere except for interface files, so in practice you almost never annotate types.
if you feed this function an empty list, what will it do? You can reason from the type signature that the only thing it possibly can do is crash, since there's no way to produce a value of type a without knowing what a is in advance.
To ensure that our function never crashes, we can encode the fact that our list is nonempty in a datatype. Ie, a nonempty list must contain a value of type a, and a (possible empty) list of as. Then instead of writing a function that crashes when it receives a nonempty list, we can write a function on nonempty lists that never crashes What I'm getting at is that, yes, this is just the "shape" of our data. But you can encode more interesting and valuable properties in that shape than might be apparent at first. There are many other interesting examples of using types to ensure that properties that we want to hold for our values, do in fact hold.Edit: I should probably add that "If it compiles it works" is absolutely not always true. It's more of a community in-joke than an actual belief. But it does turn out to be true surprisingly frequently.
Nonempty was just a simple example. You're right that it's not very commonly used (probably because it's not in Prelude), but the general culture of writing type safe interfaces and pushing guarantees to compile time %100 is a thing. A commonly used, more complicated example might be Servant, which is a web library which statically ensures your implementation meets an API spec.
Yes, I am aware that the Haskell community generally tries to push more guarantees to compile time, and that Haskell's ease of defining and using new types equivalent to existing ones but that signify some property helps a lot in this.
Still, there is some limit to this. For example, you have many libraries adding measurement units for Haskell, and many libraries doing linear algebra. But you won't find too many people doing linear algebra with measurement units, because the types of intermediate results explode too much (e.g. a type-safe unit-of-measurement matrix multiplication of 3x3 matrices has 18 type parameters, with complicated restrictions between them).
Most take Foldable or Traversable.
When it's relevant your functions can be specialized into Nonempty.
If the code is sound, it should have the same complexity anyways. I.e. putting a non-empty requirement in the function signature is just as complex as adequately handling an empty array in the code.
My personal struggle is that statically typed languages are annoying to prototype (I don't care if it segfaults on my laptop, I want to validate my idea). My struggle is prototyping in something that I can easily transition to a strictly typed version. I'm curious to try prototyping in Javascript and adding types when I move to production.
However I've tried something similar with Haskell. I think I prefer Haskell for prototyping these days. It turns out you can get a long way playing with the types. Often I spend more time prototyping the types and they guide me to the implementation which is often quite small.
It takes some training and work to get there but I think Haskell, for me, is a better prototyping language -- I don't have to deal with undefined, accidental prototype overloading, etc.
[1] https://www.youtube.com/watch?v=zCJV0xNY06o
https://news.ycombinator.com/item?id=24716903
However, when sufficiently experienced, the type system also guides the developer into good program design. Type systems for me are more a design tool than shape checker.
The checker/inferencer helps me learn about assumptions I make about data in specific parts of my code, which allows me to remodel my data types to better fit my intent. This works cyclic until things just seem to ‘fit’.
Personally at least, this allows me to write far more complicated applications than I would ever be able to do without types. And it’s just very hard for me to imagine this wouldn’t be the case for any other developer.
Disagree. You can and should be using your type system to express your system design. If it's important that your utensil has at least 3 prongs, it should only be constructible by passing at least 3 prongs; that much is trivial to do even in Java, yet alone a language with a decent (i.e. ML-family) typesystem.
Don't get me wrong though, I spent the first 10 years of programming with C and Java so I understand the comfort that a statically typed language provides. I just haven't found it to be a big enough issue over the past 7-8 years of programming in Python to really complain about it.
Frankly, if you were to ask me what the top 3 sources of my headaches in Python are, number 1, 2, and 3 would be, receiving a None value when I expected something else; but that's an issue with most languages.
Curious example, because that's absolutely something a sufficiently advanced and well-thought-out static type system can completely solve.
You're right that the type systems of C and Java do not meet this bar, however.
95% of annotations in the average haskell codebase are unneeded for compilation. People still write them, because they are a useful source of information when reading the code.
So tell me, if you feel like you don't want to write down that one line of comment which happens to be checked against the compiler, I guess you probably don't write tests or documentation either?
Why would any experienced programmer make such a blanket statement? As always, it depends.
Sometimes there is value in documentation (or whatever unchecked types in fancy languages), especially for stuff that is read a lot more, by other people, or yourself in the future.
For a lot of stuff there is no point in writing documentation. For example, if the code is obvious and pedestrian. Or if you'll just throw away everything, or refactor a month later.
And very often, writing documentation is not only a waste of time, but downright harmful, because code and documentation easily diverge.
My personal advice (not just for documentation) is to strive on the side of doing as little as you can get away with, and only introduce structure and bureaucracy if there's a clear indication that it's required.
Case in point: type aliases instead of (Haskell) newtypes. Type aliases (which are unchecked, unlike newtypes) can lead to exactly the kind of diverging that I alluded to. Look at the mess that is all the typedefs and defines in the Win32 API for example. It's extremely hard not to pass in the wrong typedefs, and there must be extremely few programmers who know which aliases are the same on every platform and which could change.
Aside, newtypes (Or the equivalent in C for example, wrapping a single member in a struct) can be very annoying. I would advicse against this, just like I'd advise against using type aliases.
> which happens to be checked against the compiler
as:
> the types aren't checked by the compiler, as my parent poster indicated.
(also type aliases are still checked. The check is less useful, but it's still a check)
> Why would any experienced programmer make such a blanket statement?
Because type annotations are less work than tests or documentation, don't suffer from falling out of sync like doc does, and because experience shows that haskell users typically find it useful to write types despite it not being necessary to compile. It would be surprising to skip types but invest in writing more complicated means of documenting and testing the software.
> 95% of annotations in the average haskell codebase are unneeded for compilation
Like type aliases. They are not distinct types, so the compiler doesn't (can't) check that you're using the "right" name.
That means most of the type annotations written exist because developers find them useful for documentation.
Doesn't it cause unintended API breaks because type changed from, say, a single type to union type, without changing any annotations?
If you have an API that exposes data to external systems, that may cause an integration breakage, indeed. That is why Haskell users like to use libs like Servant, which let you specify your API as a type. This way, if you change your system, your API has to change aswell.
Another benefit of Servant is that you can generate swagger and clients for other languages easily, which your users can use in their own build to get type safety too.
That particular 'writing against the compiler' approach lended itself to write-once, over complex type juggling code. Because why make it readable - if it compiles, it works, and if it works, there won't be any need to read it, right? Well, except you still get bugs, and then debugging anything is like making sense of brainfuck. Maybe smarter people than me can juggle a handful of types and operators simultaneously in their head - I can't.
My favourite example of 'just because it compiles it doesn't mean it works is' Maybe [a]. I've had a colleague scratch their head for hours over something like this:
This compiled, but always returned 1. Why? Well, because turns out `getUsers` returned `Maybe [User]`, and not `[User]` as the programmer expected, and somewhat asserted in their mind with types. And it also turns out that in Haskell, `length` takes a `Foldable`, and `Maybe` is a `Foldable`. Thus, a `length` of a Maybe will gladly return 0 or 1, which makes perfect sense from the point of view of the type system, but zero sense when it comes to humans. Whoops."Python provides many tools that can be used to solve programming woes. One such tool is its strong and readable syntax." Do you see how empty this sort of platitude is?
Similarly, one might say that a strong type system is a useful tool. That doesn't make it a panacea (and I don't think that this was the claim), but that doesn't mean one can't appreciate it as a good tool.
Also, you should annotate the type of your lists :)
But, if that really is the reason for Maybe [a] over [a], I'd still prefer Either SomeUsefulErrorType [a].
I know, not what you meant. But a custom sum type would not only allow you to return multiple different failure cases, you also wouldn't give it a `Foldable` instance with surprising semantics.
Plain old data types are seriously underrated these days.
My specification doesn't make a distinction between absent users and possible error responses because I assume that errors will be processed elsewhere and the getUsers function is applied as part of the "safe core" interface that is not concerned about error handling. If the specification was more precise (even more precise than in your suggestion, for instance, getUsers :: SafeIterable a => Foo -> a and length :: SafeIterable a => a -> Integer ), it would work differently.
The issue in the example has to do with the clarity of the intent encoded in the type signature, not the inherent issue of the type system that cannot guarantee safe runtime of the example. That's why I mentioned type constraints for specific iterables as a possible solution to the encoding problem.
A better example of the compiled-but-not-working case would be something that currently cannot be expressed well by the type system, something like async exceptions or access to closed resources (will be covered by linear types soon).
Haskell will never stop you from representing every value as String, and just throwing exceptions if the strings do not convert back, for example.
This statement probably gets to the heart of the issue.
A static type system isn't something that just "does things", nor can we conduct a proper analysis when the dialectic is reduced to "static vs dynamic".
The first thing to realize is that there exists a spectrum in how much we encode in our type system. Even in weaker type systems like those of Go or Java, we can make illegal states unrepresentable, encode invariants, create typesafe abstractions, etc. Or we could just treat everything as object and not make any attempt to encode anything at all in the type system.
In the former case, the aphorism "if your code compiles, it probably works", is more true than in the latter case.
The next thing to realize is that the expressiveness of a type system places a ceiling on what can be usefully encoded in that type system. So in languages like Haskell, it's possible to get closer to the "if your code compiles, it probably works" ideal, than in a language with a weaker type system. But even in Haskell, it's possible to just write everything in IO and not make any attempt to encode your constraints in the type system.
So in that sense, a type system is just a tool. If wielded effectively, it lets us offload a lot of work to the compiler in a sort of symbiotic back-and-forth. And some languages give us better static typing tools, to make that symbiosis more effective. But if we're not using simpler type systems to their fullest, we're not likely to be pushing against their limits, making it a lot harder to justify more advanced type systems.
So "if your code compiles, it probably works" shouldn't be read as a statement of fact, but as an aspirational goal, one that we can move towards by more effective use of our tools.
OK, let's stretch that metaphor a bit. A powerful type system like Haskell's is really a way of both restricting which LEGO(tm) pieces you can use and designing custom new ones. So when you design and write software in truly idiomatic Haskell, after getting all your types lined up there is no way to make anything other than a fork.
I agree that it is an oversold fallacy and types get really clunky when pushed too far. But any worthy type system prevents lot of errors. This is not generally acclaimed by empirical studies claiming only 5% improvement because they are caught at compile time.
Other benefits are enough to use a sufficiently good static type system, by the way, even for people that think type systems aren't better than TDD.
They definitely do more than that in Haskell. You can define distinct types with the same "shape", and the `newtype` keyword specifically creates a new type that has the same "shape" as an existing one, but is nominally distinct.
This nominal typing, as it's called, allow you to express intent in types. Type checking then doesn't just check whether "shapes" line up, but also whether your intentions line up. If you express contradictory intentions, you probably made a mistake.
Practically speaking, if the only types ever used in a Haskell program are `Int`, `Float`, `Char` and nested lists of those, all the type checker can ever do is tell me when I miscount square brackets or forget a `fromIntegral`. Those programs never run on the first attempt.
You won't understand those aphorisms as long as think of "type annotation" as something useless, "tax-like". I know where you come from, in languages like Java it is a tax - you repeatedly write long-winded types in different places.
With type inference, that changes. You have a REPL (or IDE integration) that you can just ask for the inferred type as soon as you write a function. Then you look at it, you think about if it expresses what you want the function to do (that already catches some errors), you copy-and-paste it, make it a bit prettier, and bam, type annotation is done. It's even on an extra line, you don't have to attach it to arguments.
Or, you design with types ("test first style", think of the type annotation as a test). You first write down the type, then you write the function, and you see if the type checker agrees. If it doesn't, you probably found a bug. Or your idea of what the function should do was wrong (has also happened to me).
And everyone who uses that system does know that type checks are very dumb checks. They only help against dumb errors. But the majority of bugs are dumb bugs (at least the majority of my bugs) - misspellings, you forget to write out some part, etc. Those get caught. The hard bugs won't - that's what tests are for. (And automated testing is again simpler in the presence of types, see Quickcheck).
> and encourage the pursuit of bad abstractions like 18-arg functions
It never does. 18-arg functions are just unwieldy. If your function grows to that point, refactor (or rather, refactor long before that). And again, the type systems helps with refactoring.
(Yes, I know I won't convince you, this debate has been going on for at least 30 years. OTOH, it doesn't hurt to see the opinion on the other side).
This is mostly because the majority of software developers only know Java and are averse to learning new languages. Also, companies chose Java at one point and are now taking next to no risks by introducing new languages (not even languages on the JVM). They can also just fire and rehire Java developers because there is a large pool of available developers that they can just plug-n-play, so to speak. They're not interested training people.
* Fail a project in Java, and you have a project failure. Fail a project in Haskell, and you have a Haskell failure.
* Management has a large veto power, and management usually favors technologies they are used to, or used to use themselves.
* Management also favors their ease of work with the tech. In practice, they often hold the belief that it's much easier to find java developers than it is to find haskell developers.
* Haskell suffers from a reputation of being "academic" and "hard to do real things with".
In practice, I've done great commercial projects in Haskell, and the technology was worth using. When done right, recruiting for haskell projects was much faster than recruiting for any mainstream language. On the flipside, it was sometimes hard to get adequate corporate stewardship for the language (for instance, managers/HR insisting they found no one for the job after publishing through the usual channels when you know half a dozen people at the local meetup that would jump ship if offered a position).
Other developers working in related areas are using Ruby and Python but my software runs a lot faster.
Part of that may be the continuing influence of PG: I both learnt Lisp and found HN because of his essays, and I suspect I'm not the only one.
But also, the HN community generally has an academic fondness for new and niche languages, so a language's popularity on HN is a poor proxy for its popularity in the business world.
(A last point: I think the diminished importance of Lisp in the last decades is less due to an inherent flaw of the language and more to what you might call "accidents of history" - particularly its close association with early attempts at AI.)
I find I would rarely reach for macros in Haskell, but that I sometimes would like to. Lisp macros do overlap with what you can achieve with Haskell things like typeclasses, laziness, monads and template Haskell, but the overlap is far from 100%.
The author of Hackett has a video about it: https://youtu.be/5QQdI3P7MdY
The Common Lisp implementations and compilers keep on getting better with time and they produce more and more compile-time warnings (especially SBCL).
Which case I clearly see here.
Now, some discourage the use of type declarations in Common Lisp (for a variety of reasons, one being the limited portability as less clever compilers don't warn against mismatch, but might produce broken code) and suggest the (run-time type testing) CHECK-TYPE (standard) macro instead. SBCL's type inference (at least in recent versions) is sufficiently clever to detect mismatches at compile time, when the type is (implicitly) announced using CHECK-TYPE.
Common Lisp isn't my usual dialect but I'd've expected something more like:
Note: pseudocode typed straight into the comment box, and I dropped the newline feature, but hopefully a bit more illustrative of what I'd consider 'normal' lisp.(comments, criticisms and thrown fruit welcome)
I was trying to elucidate a different structure.
I tend to do single assignment and minimal mutations in any language though, and try and push my side effects (I'm aware the string write is encapsulated therefore not a side effect) as far out to the edge as possible.
As an example, in perl (which is probably my most-used language, so I'm 100% on board with "more than one way to do it" ;) I'd've written:
(my lisping has indeed been significantly in scheme, plus dabbling in other dialects and a tendency to prototype things in operative lisps derived from Shutt's kernel ... which is why I did, indeed, ask for it, so thank you for the reply)Every REPL form sent recompiles the running program, I don't want that fast loop to be force lagged by the program checking types
I'm more than happy for that to happen at edit time, independent of compile time outside of my application process
So I heavily encourage using clj-kondo to check for mistakes doing primitive type checking etc
To reiterate, I love static analysis but I also need control over how and when it runs, I like it in CI and during editing
Otherwise it comprises one of the best things about those old languages, (Smalltalk lisp etc) being able to dev your application by injecting code and recompiling while the app is still running
The feedback loop while coding a live image is hard to explain but anything that makes it slower is disastrous for me, I can lose focus so quick
When deving Clojure idiomatically partial recompile happens at a much greater frequency than most languages, compile time is very important
I'm not sure if what Haskell has is quite the same, abilities but you can hot-reload modules at runtime in GHCi, and FWIW, I've always found the type checker to be pretty zippy.
https://vimeo.com/230220635
I agree, its pretty cool, and actually pretty fun.
This lets you do things like (stupid trivial example follows, Common Lisp not Clojure) write a function from inside out in the REPL, then move the code to source:
Imagine that these were more complicated things, like you're using a web API and getting JSON files and parsing them. You can develop it interactively, and as you verify that things are correct, you can migrate them to functions (and add tests to really do the verification part).As the author says, the inbuilt standard library is hopelessly too small for modern requirements - even basics like string manipulation are only cursorily covered. And most of the third-party libraries out there are single-person projects, out of date, undocumented, or all three...
The language itself is still fantastic and, IMO, still superior to most other languages out there. But the lack of decent libraries is a major productivity killer.
I was thinking about mentioning that list, actually. Yes, there are libraries around for most common tasks. However, there are three things I'm not satisfied with:
1) I expect common tasks to be covered by the standard library. Third-party libraries are for specialised tasks.
2) There's often three or four mediocre libraries for the a given task instead of one really good one. Of course there are some "default libraries" (alexandria, bordeaux-threads, etc.) but too often you basically need to pick one at random and hope for the best. Best example: GUI libraries.
3) Most libraries are side-projects by individual developers or only have small teams. Which means: poor documentation and (in some cases) frequent breaking changes or random interruptions in development. The lack of documentation is not quite as bad as it sounds, because the source code tends to be highly readable. But still, the lack of polish and especially stability is not exactly trust-inspiring.
The CL ANSI standard really needs an overhaul to significantly expand the number of inbuilt functions. The fact that my 1990 edition of CLtL2 still adequately describes any modern CL implementation is a testament to the thoroughness of its language design, but also to the lack of change and expansion in the 30 years since.
Alternately, the CL community should develop its own semi-official stdlib - like the R community did with the tidyverse. A single package that you can install via Quicklisp that pulls in a curated list of high-quality libraries, ready for use. The CL standard might not come "batteries included" any time soon, but a "battery expansion pack" would still be a major improvement over "bring your own batteries" ;-)
That's an idea I would actually like to develop further :D Not sure HN is the right place though, so I moved it to r/Common_Lisp: https://www.reddit.com/r/Common_Lisp/comments/j7vd25/a_curat...
Im not sure his code is idiomatic SBCL but it could be written more succintly and functionally by most LISPers.
To the point on static/dynamic typing, I disagree. But its a discussion thats been going on for so long that Ive concluded its mostly a matter of taste. For Clojurians who like static typing, Spec packs a big punch without removing flexibility.
Still, Haskell is pretty great but Im more efficient in Clojure, Your mileage will vary.