I found one of the perceived weaknesses of Clojure (in this article), it being dynamically typed, is a tradeoff rather than a pure negative. But it applies that tradeoff differently than dynamic languages I know otherwise and that difference is qualitative: It enables a truly interactive way of development that keeps your mind in the code, while it is running. This is why people get addicted to Lisp, Smalltalk and similar languages.
> To understand a program you must become both the machine and the program.
- Epigrams in Programming, Alan Perlis
Two of the big advantages of (gradually-) typed languages are communication (documentation) and robustness. These can be gained back with clojure spec and other fantastic libraries like schema and malli. What you get here goes way beyond what a strict, static type systems gets you, such as arbitrary predicate validation, freely composable schemas, automated instrumentation and property testing. You simply do not have that in a static world. These are old ideas and I think one of the most notable ones would be Eiffel with it's Design by Contract method, where you communicate pre-/post-conditions and invariants clearly. It speaks to the power of Clojure (and Lisp in general) that those are just libraries, not external tools or compiler extensions.
In 2021, I find it hard to justify using a dynamically typed language for any project that exceeds a few hundreds of lines. It's not a trade off, it's a net loss.
The current crop of statically typed languages (from the oldest ones, e.g. C#, to the more recent ones, e.g. Kotlin and Rust) is basically doing everything that dynamically typed languages used to have a monopoly on, but on top of that, they offer performance, automatic refactorings (pretty much impossible to achieve on dynamically typed languages without human supervision), fantastic IDE's and debuggability, stellar package management (still a nightmare in dynamic land), etc...
Yeah, I had a fairly large (about a year of solo dev work) app that I maintained both Clojure and F# ports of, doing a compare and contrast of the various language strengths. One day I refactored the F# to be async, a change that affected like half the codebase, but was completed pretty mechanically via changing the core lines, then following the red squigglies until everything compiled again, and it basically worked the first time. I then looked at doing the same to the Clojure code, poked at it a couple times, and that was pretty much the end of the Clojure port.
Hey, so my my career path has been C# (many years) -> F# (couple years) -> Clojure (3 months). I understand multithreading primarily through the lens of async/await, and have been having trouble fully grokking the Clojure's multithreading. One of the commandments of async/await is don't block: https://blog.stephencleary.com/2012/07/dont-block-on-async-c...
Which is why the async monad tends to infect everything. Clojure, as far as I can tell so far, doesn't support anything similar to computation expressions. So I'm guessing your "poked at it a couple times" was something like calling `pmap` and/or blocking a future? All my multithreaded Clojure code quickly blocks the thread... and I can't tell if this is idiomatic or if there's a better way.
Not even. It was opening it, looking, realizing it would take a couple weeks, and going back to F#. I did this a couple times before fully giving up.
IIRC/IIUC, Clojure's async support is closer to Go's (I've never used go), in the form of explicit channels. Though you can wrap that in a monad pretty easily, which I did for fun one day (https://gist.github.com/daxfohl/5ca4da331901596ae376). But neither option was easy to port AFAICT before giving up.
Note it's possible that porting async functionality to Clojure may have been easier that I thought at the time. Maybe adding some channels and having them do their thing could have "just worked". I was used to async requiring everything above it to be async too. But maybe channels don't require that, and you can just plop them in the low level code and it all magically works. A very brief venture into Go since then has made me wonder about that.
Yeah, quite possible. I haven't worked on the project in ~six years and lost all context, but I'd revisit it and see if perhaps there was a simple solution if any of it was still current.
While core.async pays homage to go, it's simply not go, and it's harder to work with, and generally the changes are going to be more invasive, and looking around at modern resources, I don't see anything that indicates much has changed. So while I might have been more efficient if I'd had the go mental model, that was definitely not the only problem. Migrating was too much to do in my fairly large project, hunting and pecking at each instance I made an async call. Whereas with F# it was truly mechanical and hard to mess up, as I described above.
Multi-threaded code is normally not implemented in an async style, but instead is done where each thread of execution is synchronous.
Async style comes into play generally for languages that lack real threads, or as a way to manage callbacks (even if single threaded), or in order to wait for blocking IO without the need for a real thread.
So ya, it's idiomatic to use blocking to coordinate between different threads in Clojure, same as Java.
Java decided to work on making stackful coroutines instead of stackless like C#. That requires a lot more work, but should be coming eventually to Java. At that point, your "blocking" code in Clojure will no longer block a real thread, but a lightweight fiber instead. But patience is needed for it.
In the meantime, if you're dealing with non-blocking IO that operates with callback semantics or other callback style code, what you can do in Clojure to make working with that easier is use one of:
Thanks for the reply - what you say makes a lot of sense. I watched Rich's talk on Async and was like... "cool so `core.async` follows this pattern right?!" ...not quite.
I'll check out your other links though, much appreciated. Also hearing that I should just be okay with blocking is well, good to hear explicitly.
> In 2021, I find it hard to justify using a dynamically typed language for any project that exceeds a few hundreds of lines. It's not a trade off, it's a net loss.
Only if you are skimping on tests. There's a tradeoff here - "dynamically typed" languages generally are way easier to write tests for. The expectation is that you will have plenty of them.
Given that most language's type systems are horrible (Java and C# included) I don't really think it's automatically a net gain. Haskell IS definitely a net gain, despite the friction. I'd argue that Rust is very positive too.
Performance is not dependent on the type system, it's more about language specification (some specs paint compilers into a corner) and compiler maturity. Heck, Javascript will smoke many statically typed languages and can approach even some C implementations(depending on the problem), due to the sheer amount of resources that got spent into JS VMs.
Some implementations will allow you to specify type hints which accomplish much of the same. Which is something you can do on Clojure by the way.
Automatic 'refactorings' is also something that's very language dependent. I'd argue that any Lisp-like language is way easier for machines to process than most "statically typed" languages. IDEs and debugability... have you ever used Common Lisp? I'll take a condition system over some IDE UI any day. Not to mention, there's less 'refactoring' needed.
Package management is completely unrelated to type systems.
Rust's robust package management has more to do with it being a modern implementation than with its type system. They have learned from other's mistakes.
Sure, in a _corporate_ setting, where you have little control over a project that spans hundreds of people, I think the trade-off is skewed towards the most strict implementation you can possibly think of. Not only type systems, but everything else, down to code standards (one of the reasons why I think Golang got popular).
In 2021, I would expect people to keep the distinction between languages and their implementations.
Here's what I've noticed with my tests and dynamic languages. I'll get type errors that static typing would have caught. However those errors occur in places I was missing testing of actual functionality. Had I had the functionality tests, then the type error would have been picked up by my tests. And had I just had static typing, the type system would not have been enough to prove the code actually works, so I would have needed tests anyways.
Point being, I don't really buy that a static type system saves me any time writing and maintaining tests, because type systems are totally unable to express algorithms. And with a working test suite (which you will need regardless of static vs dynamic) large refactors become just as mechanical in dynamic languages as they are in static languages.
> type systems are totally unable to express algorithms
You don't know much about types if you think that.
As for dynamic typing "helping" you to find code that you need to write tests for: There are already far more sophisticated static analysis tools to measure code coverage.
doubler :: Num a => [a] -> [a]
doubler xs = take 2 xs
Passes the type checker, thanks type system! /s
I like static typing, but static typing advocates seriously overstate how much protection the type system gives you. Hickey really said it best: "We used to say 'If it compiles it works' and that's as true now as it was then."
As for dynamic typing "helping find code to write tests", that's not a feature, it's a huge downside. Neither side is perfect, but in my experience the benefits of the static checker are overblown since I need to write tests anyways. And also like you say, there's a variety of great static analysis tools you should be using as well.
Proving that your program is consistent is only one of the many benefits that a static type system brings you.
I'd say the main one is that it enables automatic refactorings, which are mathematically impossible to achieve when you don't have type annotations.
Thanks to automatic refactorings, code bases are easier to maintain and evolve, as opposed to dynamically typed languages where developers are often afraid to refactor, and usually end up letting the code rot.
It's also a great way to document your code so that new hires can easily jump on board. It enables great IDE support, and very often, unlocks performance that dynamically typed languages can never match.
> I'd say the main one is that it enables automatic refactorings, which are mathematically impossible to achieve when you don't have type annotations.
Yeah, it's really not impossible. Maybe in theory, it's "mathematically impossible", but in practice, doing a search on your local codebase and understanding the code you find makes it easy to do refactors too. Dynamic languages also can help making refactoring obsolete, as you can create data structures that doesn't matter if they are User or Person, as long as it has a Name, print the name (or whatever, just a simple example) whereas with a static type system, you'd have to change all the User to Person. You're basically locking your program together, making it coupled and harder to change.
> It enables great IDE support
Is there anything specific that IDEs support for static typing that you can't have for dynamic languages? I mostly work with Clojure and have everything my peers have when using TypeScript or any other typed language.
> unlocks performance that dynamically typed languages can never match
I think there is a lot more to performance than just types. Now the TechEmpower benchmarks aren't perfect, but a JS framework is at the 2nd place in the composite benchmark, beating every Rust benchmark. How do you explain this if we consider your argument that types will for sure make everything faster and more efficient?
I'm not sure when did this become about typing systems promising you'll never write tests? IMO nobody says that.
Let me give you one example.
When I'm coding in Rust and I forget to match on one of my sum type's variants, the compiler will immediately yell at me.
When I'm coding in Elixir, the compiler doesn't care if I do exhaustive pattern matching because it doesn't know all possible return values. In these conditions it's extremely easy to not write code that deals with a return value that appears rarely.
That was meant as a response to tsss apparently overvaluing his type checks.
The pattern matching example is one that often comes up talking about typing. Yes it's great that the type checker finds all the places you didn't deal with your new sum type varient...except here's the rub. All that code was working just fine before. Your static type checker is forcing a bunch of code that never needed to know or care about certain values onto all places where you used pattern matching. I don't think this speaks to the value of static typing, I think it suggests that pattern matching is a bad idea that leads to overly coupled code where parts of the system that really shouldn't need to know about each other are now forced to deal with situations they don't care about.
> Your static type checker is forcing a bunch of code that never needed to know or care about certain values onto all places where you used pattern matching.
It's not "forcing" anything, you are evolving your program and the compiler is helping you not play a whack-a-mole by actually telling you every place that must be corrected in order to account for the change.
Wasn't aware that evolving a project is called forcing. :P
> I think it suggests that pattern matching is a bad idea that leads to overly coupled code where parts of the system that really shouldn't need to know about each other are now forced to deal with situations they don't care about.
That's a super random statement, dude. If a sum type change makes 7 places in your code not compile then obviously those pieces of code do care about it -- you wouldn't write it that way if it didn't. Nobody put a gun on your head forcing you to include the sum type in these places in the code just because, right?
Overall I am not following your train of thought. You seem to be negatively biased. I've seen from my practice only benefits by enforcing exhaustive pattern matching. Many times I facepalmed after I got a compiler error in Rust and was saying "gods, I absolutely would've missed that if I wrote it in a dynamic language".
You can very easily write a "safe" version of that function that will not type check and in this case you don't even need dependent types. So: bad example on your part.
So you're saying without dependent types you can express in a type a function that will return double each element in the list thus removing the need to test the function?
If you can do that, that's awesome, but I'm not seeing how.
I meant that you can write a version of your function with the same definition that will not type check since `take 2` is illegal for lists of length less than 2.
As for a function that will "double" a list, i.e. turn [1,2] into [1,1,2,2]: That is definitely possible with dependent types as they can express arbitrary statements. I'm not sure if you can do it without dependent types, but I'm inclined to say yes: something like an HList should work. Universal quantification over the HList parameters will ensure that the only way to create new values of the parameter types is to copy the old ones, as long as you disallow any form of `forall a. () -> a` in the type system.
Something like this, which is just the `double` function lifted into the universe of types, _might_ work, though its utility is questionable:
type family DoubleList xs :: 'HList -> 'HList where
DoubleCons ('HCons x ': xs) = 'HCons x ': 'HCons x ': Double xs
DoubleNil 'HNil = 'HNil
So totally different function, I meant by double to turn [1,2] into [2,4], however that's a really neat example. I hadn't seen the family extension in Haskell before. You're right there is a ton more to type systems than I was aware of. I was following some links in this thread and found this as well: https://www.parsonsmatt.org/2017/10/11/type_safety_back_and_....
I was less than impressed with type systems because like the blog post says, they tend to just kick the can down the road. The blog post uses a technique like you did in your example where rather than emitting more complicated type, they use the type system to protect the inputs of the function thus moving handling with the problem to the edges of the system which seems like a huge win. Between your example and that post I'm starting to see what people mean when they talk about programming in types, as its almost like the type system become a DSL with its own built-in test suite with which to program rather than a full programming language.
Either way very thought provoking, thank you for your responses.
Hmm, I think you can do that too, but you'd have to assign each int value its own singleton type, which would be ridiculous and not gain you anything since you're just moving the logic up one level in the hierarchy of universes.
> what people mean when they talk about programming in types
If the type system is powerful enough then you can express any function at the type level. Some languages with universal polymorphism make no difference between types and terms. Any function can also be used at the type-level, kind-level and so on. Though usually just defining a simple wrapper type with smart constructor will get you 80% of the way in a business application with 2% of the effort of real type-level programming.
We can debate this forever, but all I can say is that at my work we have equal part Java and Clojure, and we have some Kotlin and sole Scala as well. Out of all of them, Clojure does not cause us anymore issues, it doesn't take us any longer to add features, it doesn't perform any worse, and it doesn't have any more defects than the others.
My conclusion is that it's a matter of personal preference honestly. Those are all really good languages. Personally I have more fun and enjoy using Clojure more. I would say I tend to find I'm more productive in it, but I believe that's more a result of me finding using it more enjoyable then anything else.
I don't pick Clojure for its dynamic typing, I pick it for other reasons. I've tried Haskell but it really doesn't seem to mesh with the way I tend to develop a program. But I would love to have more static languages with the pervasive immutability of Clojure.
You are forgetting that Smalltalk with its image has the visibility of the whole world AST, so its dynamism has some helping metadata to go along the OS features.
Also it wasn't perfect, hence why Strongtalk was born, the remains of which now live on Hotspot.
My question is how does that work in a dynamically typed language? In static typed language we can know scope & type of a variable and we can't change much in runtime.
In Clojure you know the number of arguments to a function and the name of functions and variables, and the code is all very well structured as an AST (being a Lisp).
So you can do a lot of refactorings with that such as:
Rename function, rename variable, rename namespace, extract constant, extract function, extract local variable, extract global variable, convert to thread-first, convert to thread-last, auto-import, clean imports, find all use, inline function, move function/variable to a different namespace, and some more.
The only thing is you can't change the "type" of something and statically know what broke.
I believe you're mistaken, but please explain otherwise?
None of those seem to require type information from my reasoning (and are also all available in Emacs for Clojure)
For example, moving a function from one namespace to another, you know where this function is being used from the require declarations, and you know where you've been told to move it too and where it currently resides. So you can simply change the old require pointing to its old namespace to point to the new namespace and cut/paste the function from the old to the new. Nothing requires knowing the type or the arguments or the return value of the function.
Even Smalltalk's refactoring browser made mistakes which humans had to fix by hand. Which is not surprising, because in the absence of type annotation, the IDE doesn't have enough knowledge to perform safe refactorings.
That blog is talking about refactoring a method, not a function.
In Clojure, I'm talking about renaming a function, which can be done without types.
See the difference is that with a method:
x.f()
You have to know the type of `x` to find the right `f`, but with a function in Clojure:
(ns foo
(:require [a :refer [f]]))
(f x)
The location of `f` is not dependent on the type of `x`, you known statically that this `f` is inside the namespace `a`, because of the require clause that says that in `foo`, `f` refers to the `f` inside of `a`.
And this is unambiguous in Clojure because there cannot be more than one `f` inside `a`.
If you had two `f` this would be the code in Clojure:
(ns a)
(defn f [] "I'm in a")
(ns b)
(defn f [] "I'm in b")
(ns foo
(:require [a :refer [f]]
[b :refer [f]
:rename {f bf}]))
(f x)
(bf x)
You're forced to rename the other f, and now it's clear statically again that `bf` is the `f` from `b` and `f` is the one from `a`, no need to know the type of `x` for it.
You are pushing fud about not having typing systems at all... they are valuable to automated systems for introspection to some degree.
However you are talking about typing as if all typing is static - static typing has little value beyond warm and fuzzies on the developers part, dynamic typed systems are able to perform just as well. At which point, the dynamic part can allow you to mostly drop the types.
statically typed systems, you will note, tend to come with ecosystems dedicated to using the static bits as little as possible. And they provide no guarantee of correctness.
Yes, new devs might be able to latch onto some specific typing a bit better, but I don't care if you have all the automated refactors and a hundred new employees, if your codebase sucks and is incorrect, your static analysis is worth didly squat.
It's your opinion though, there's nothing scientific about what you're saying.
Take mocking for example, in Ruby/Rails it's a breeze. In Java you need to invent a dependency injection framework (Spring) to do it.
The best response from the statically-typed world is functional programming and explicit dependencies (Haskell, OCaml, F#), which makes mocking unnecessary most of the time. OOP (Java, C#) is not the true standard for static-typing, just the most common one.
I think you are mistaken. Mocking and DI frameworks are two unrelated concepts. There is nothing in Java that forces you to use a DI framework, e.g., Spring if you want to use mocks during testing.
In theory, I agree, but I don't think that holds terribly true in practice.
One of the ideas behind IoC frameworks (which build on top of DI) is that you could swap out implementation classes. For a great deal of software (and especially in cloud-hosted, SaaS style microservice architecture) the test stubs are the only other implementations that ever get injected.
Most code bases could ditch IoC if Java provided a language-level construct, even if that construct were only for the test harness.
Java has a mechanism, just pass alternate implemenations in constructors. If you must, a setter method. For most code you don't need to bring in the overhead of Spring, and @Autowired isn't really more convenient typing wise. Plus your unit tests become trivial, they're just POJOs with @Test annotations.
Spring is great when you need that dynamic control at runtime (especially when code dependencies are separated by modules) but you're just aping what good dynamic languages like Clojure or Common Lisp give you for free. But I can't complain too much, developing modern Java with its popular frameworks and with JRebel is getting closer to the Lisp experience every year, I'd rather have that than for Java to remain stagnate like in its 1.6/1.7 days.
Let's say I have a class called User and in it a method that says the current time. So User#say_current_time
which simply accesses the Date class (it takes no arguments).
Can you show me how you would mock the current time of that method in Java?
Without using a mock framework, assuming User#say_current_time isn't a private or static method then:
final Date testDate = someFixedDate;
User testUser = new User() {
@Override
Date say_current_time() {
return testDate;
}
};
If it is private and/or static, you can get around it without having to change the code, but if you own the code, you should just do that... Often the change will be as simple as replacing some method's raw usage of Date.now() with a local say_curent_time() method that uses it or some injected dependency just so you can mock Date.now() without hassle.
But your point further down that in Java you have to think about your code structure more to accommodate tests is valid. I think it's easy to drink the kool-aid and start believing that many code structuring styles that enable easier testing in Java are actually very often just better styles regardless of language, but you're not going to really see the point if you do nothing but Ruby/JS where you can get away with not doing such things for longer. Mostly it has to do with dynamic languages offering looser and later and dynamic binding than static languages (which also frequently makes them easier to refactor even if you don't have automated tools). One big exception is if your language supports multiple dispatch, a lot of super ugly Java-isms go away and you shouldn't emulate them. The book Working Effectively with Legacy Code is a good reference for what works well in Java and C++ (and similar situations in other languages), it's mostly about techniques for breaking dependencies.
I'll take clean contractual interfaces (aka actual principle of least surprise) over "I can globally change what time means with one line of code!" on large projects every time.
If you want to use DI, in java 8 you could inject a java.time.Clock instance in the constructor and provide a fixed instance at the required time in your test e.g.
Instant testNow = ...
User u = new User(Clock.fixed(testNow, ZoneOffset.UTC));
u.sayCurrentTime();
although it would be better design to have sayCurrentTime take a date parameter instead of depending on an external dependency.
In my experience the need to mock out individual methods like this is an indication that the code is badly structured in the first place. The time source is effectively a global variable so in this example you'd want to pass the time as a parameter to `sayCurrentTime` and avoid the need to mock anything in the first place. A lot of C#/java codebases do seem to make excessive use of mocks and DI in this way though.
OK. first I could be ignorant about Java since I haven't touched it in more than a decade. Which library is doing that? And also what is mock(User.java) returning - is it an actual User instance or a stub? I want a real User instance (nothing mocked in it) with just the one method mocked.
And again if this is possible I will admit ignorance and tip my hat at the Java guys.
It's Mockito [1], which has been a standard for a while. There are other libraries and they use different strategies to provide this kind of functionalities (dynamic proxies, bytecode weaving, annotation processing, etc...).
I think what you want is a "spy" (partial mock), not a full "mock", but yes, both are possible. You can partially mock classes, i.e., specific methods only. Syntax is almost the same, instead of mock(User.class) you write spy(User.class).
The fact that there are such libraries in existence means that there is no pain associated to this particular activity. Not only do you get great mocking frameworks, they are actually very robust and benefit from static types.
Mocking dynamically typed languages is monkey patching, something that the industry has been moving away from for more than a decade. And for good reasons.
> The fact that there are such libraries in existence means that there is no pain associated to this particular activity
I can say the same about Rails + RSpec. It exists therefore it's good.
> Mocking dynamically typed languages is monkey patching, something that the industry has been moving away
That's a reach. There are millions of javascript/python/php/ruby/elixir devs that don't use types or annotations. They mock. "The industry" isn't one cohesive thing.
Not only this, but the programming style where you pass around dictionaries / maps for everything yet have expectations about what keys they contain works just as easily in JS, and with TypeScript or Flow you get a lot more help from the compiler than you do using spec (as I understand it).
Although you are right, the Clojure community probably by and large agrees with you. That is why everyone is excited about spec - it looks a lot like a type system for Clojure.
Can you elaborate why? To be honest, I don't have experience with large-scale Clojure codebases, but I have my fair share working on fairly hefty Python and Perl projects, and I tend to think that the parent commenter is mostly right. What makes you think they are incorrect?
Not who you are responding to, but the common idea that static types are all win and no cost has become very popular these days, but isn't true, it's just that the benefits of static typing are immediately apparent and obvious, but their costs are more diffuse and less obvious. I thought this was a pretty good write up on the subject that gets at a few of the benefits https://lispcast.com/clojure-and-types/
Just to name some of the costs of static types briefly:
* they are very blunt -- they will forbid many perfectly valid programs just on the basis that you haven't fit your program into the type system's view of how to encode invariants. So in a static typing language you are always to greater or lesser extent modifying your code away from how you could have naturally expressed the functionality towards helping the compiler understand it.
* Sometimes this is not such a big change from how you'd otherwise write, but other times the challenge of writing some code could be virtually completely in the problem of how to express your invariants within the type system, and it becomes an obsession/game. I've seen this run rampant in the Scala world where the complexity of code reaches the level of satire.
* Everything you encode via static types is something that you would actually have to change your code to allow it to change. Maybe this seems obvious, but it has big implications against how coupled and fragile your code is. Consider in Scala you're parsing a document into a static type like.
case class Record(
id: Long,
name: String,
createTs: Instant,
tags: Tags,
}
case class Tags(
maker: Option[String],
category: Option[Category],
source: Option[Source],
)
//...
In this example, what happens if there are new fields on Records or Tags? Our program can't "pass through" this data from one end to an other without knowing about it and updating the code to reflect these changes. What if there's a new Tag added? That's a refactor+redeploy. What if the Category tag adds a new field? refactor+redeply. In a language as open and flexible as Clojure, this information can pass through your application without issue. Clojure programs are able to be less fragile and coupled because of this.
* Using dynamic maps to represent data allows you to program generically and allows for better code reuse, again in a less coupled way than you would be able to easily achieve in static types. Consider for instance how you would do something like `(select-keys record [:id :create-ts])` in Scala. You'd have to hand-code that implementation for every kind of object you want to use it on. What about something like updating all updatable fields of an object? Again you'll have to hardcode that for all objects in scala like
case class UpdatableRecordFields(name: Option[String], tags: Option[Tags])
def update(r: Record, updatableFields: UpdatableRecordFields) = {
var result = r
updatableFields.name.foreach(r = r.copy(name = _))
updatableFields.tags.foreach(r = r.copy(tags = _))
result
}
all this is specific code and not reusable! In clojure, you can solve this for once and for all!
Your third point about having to encode everything isn’t quite true. Your example is just brittle in that it doesn’t allow additional values to show up causing it to break when they do. That’s not a feature of static type systems but how you wrote the code.
This blog post[1] has a good explanation about it, if you can forgive the occasional snarkyness that the author employs.
In a dynamic system you’re still encoding the type of the data, just less explicitly than you would in a static system and without all the aid the compiler would give you to make sure you do it right.
It's important to note that this article talks about something that is missing from most statically typed languages.
It's best to refrain from debating static VS dynamic as generic stereotype and catch all.
You need to look at Clojure vs X, where if X is Haskell, Java, Kotlin and C#, what the article talks about doesn't apply and Clojure has the edge. If it's OCaml or F# than they in some scenarios don't suffer from that issue like the others and equal Clojure. But then there are other aspects to consider if your were to do a full comparison.
In that way, one needs to understand the full scope of Clojure's trade offs as a whole. It was not made "dynamic" for fun.
Overall, most programming languages are quite well balanced with regards to each other and their trade-offs. What matters more is which one fits your playing style best.
I think many peoples' experience is that most real world data models aren't as perfect as making up toy examples in blog posts. Requirements and individuals change over time. You can make an argument that in a perfect world with infinite time and money that static typing may be better because you can always model things precisely, but whether you can do that practically over longer periods of time should be a debatable question.
I've seen this article and I applaud it for addressing the issue thoroughly but I still am not convinced that static typing as we know it is as flexible and generic as dynamic typing. Let's go at this from an other angle, with a thought experiment. I hope you won't find it sarcastic or patronizing, just trying to draw an analogy here.
So, in statically typed languages, it is not idiomatic to pass around heterogeneous dynamic maps, at least in application code, like it is in Ruby/Clojure/etc. But one analogy we can draw which could drive some intuition for static typing enthusiasts is to forget about objects and consider lists. It is perfectly familiar to Scala/Java/C# programmers to pass around Lists, even though they're highly dynamic. So now think about what programming would be like if we didn't have dynamic lists, and instead whenever you wanted to build a collection, you had to go through the same rigamarole that you have to when defining a new User/Record/Tags object.
So instead of being able to use fully general `List` objects, when you want to create a list, that will be its own custom type. So instead of
val list = List(1,2,3,4)
you'll have to do:
case class List4(_0: Int, _1: Int, _2: Int, _3: Int)
val list = List4(1,2,3,4)
This represents what we're trying to do much more accurately and type-safely than with dynamic Lists, but what is the cost? We can't append to the list, we can't `.map(...)` the list, we can't take the sum of the list. Well, actually we can!
So what's the problem? I've shown that the statically defined list is can handle the cases that I initially thought were missing. In fact, for any such operation you are missing from the dynamic list implementation, I can come up with a static version which will be much more type safe and more explicit on what it expects and what it returns.
I think it's obvious what is missing, it's that all this code is way too specific, you can't reuse any code from List4 in List5, and just a whole host of other problems. Well, this is pretty much exactly the same kinds of problems that you run into with static typing when you're applying it to domain objects like User/Record/Car. It's just that we're very used to these limitations, so it never really occurs to us what kind of cost we're paying for the guarantees we're getting.
That's not to say dynamic typing is right and static typing is wrong, but I do think that there really are significant costs to static typing and people don't think about it.
I’m not sure I follow your analogy. I think the dynamism of a list is separate from the type system. I can say I have a list of integers but that doesn’t limit its size.
I can think of instances where that might be useful and I think there’s even work being done in that direction in things like Idris that I really know very little about.
There are trade offs in everything. I’m definitely a fan of dynamic type systems especially things like Lisp and Smalltalk where I can interact with the running system as I go, and not having to specify types up front helps with that. Type inference will get you close to that in a more static system, but it can only do so much.
The value I see in static type systems comes from being able to rely on the tooling to help me reason about what I’m trying to build, especially as it gets larger. I think of this as being something like what Doug Englebert was pointing at when he talked about augmented intelligence.
I use Python at work and while there are tools that can do some pretty decent static analysis of it, I find myself longing for something like Rust more and more.
Another example I would point to beyond the blog post I previously mentioned is Rust’s serde library. It totally allows you to round trip data while only specifiying the parts you care about. I don’t think static type systems are as static as most like to think. It’s more about knowns and unknowns and being explicit about them.
I believe your comments provided a good insight into your approach to programming. I may be wrong in my understanding, but let me elaborate.
You expect your programming language to be a continuation of your thoughts, it should be flexible and ductile to your improvisations. You see static typing as a cumbersome restricting bureaucracy you have to obey to.
Whereas I see type system like a tool that helps to structure my thoughts, define the rules and interfaces between construction blocks of my program. It is a scaffolding for a growing body of code. I found that in many cases, well defined data structures and declarations of functions are enough to clearly describe how some piece of code is meant to work.
It seems we developed different preferred ways of writing code, maybe, influenced by our primary languages, features of character, type of software we create. I used Scala for several years, but recently I regularly use Python. Shaping my code with dataclasses and empty functions is my preferred way to begin.
It is absolutely possible to have the same type for values that have the same shape.
You can have a `Map k v` that is a record that dynamic languages have that they call object/map.(make k/v Object or Dynamic if you want)
You don't need to create a new type with precise information if you just want that(no you don't need to instantiate type params everywhere). There is definitely limitations in type-systems (requiring advanced acrobatics) but most programs don't run into them and HM type system (https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_sy...) has stood the test of time.
Let me address your criticism from Scala's point of view
> they are very blunt
I'm more blunt than the complier usually. I really want 'clever' programs to be rejected. In rare situations when I'm sure I know something the complier doesn't, there are escape hatches like type casting or @ignoreVariace annotation.
> the problem of how to express your invariants within the type system
The decision of where to stop to encode invariants using the type system totally depends on a programmer. Experience matters here.
> Our program can't "pass through" this data from one end to an other
It's a valid point, but can be addressed by passing data as tuple (parsedData, originalData).
> What if there's a new Tag added? What if the Category tag adds a new field?
If it doesn't require changes in your code, you've modelled your domain wrong - tags should be just a Map[String, String]. If it does, you have to refactor+redeploy anyway.
> What about something like updating all updatable fields of an object
I'm not sure what exactly you meant here, but if you want to transform object in a boilerplate-free way, macroses are the answer. There is even a library for this exact purpose: https://scalalandio.github.io/chimney/! C# and Java have to resort to reflection, unfortunately.
> In a language as open and flexible as Clojure, this information can pass through your application without issue. Clojure programs are able to be less fragile and coupled because of this.
Or this can wreak havoc :) Nothing stops you from writing Map<Object, Object> or Map[Any, Any], right?
That's true! But now we'll get into what is possible vs what is idiomatic, common, and supported by the language/stdlib/tooling/libraries/community. If I remember correctly, Rich Hickey did actually do some development for the US census, programming sort of in a Clojure way but in C#, before creating Clojure. But it just looked so alien and was so high-friction that he ended up just creating Clojure. As the article I linked to points out, "at some point, you're just re-implementing Clojure". That being said, it's definitely possible, I just have almost never seen anyone program like that in Java/Scala.
Agreed. I feel Lisps and SmallTalk are dynamic done right. I think the other language features that you use also influence the value from dynamic or static types. For OOP style, static types are a huge asset for refactoring and laying our architecture. On the other hand, immutable data and stateless functions (as idiomatic in clojure) make them less necessary, and also work great together with interactive development.
Except, of course, that specs are only tested correct, not proven correct like types would be. Types (in a reasonable static type system, not, say, C) are never wrong. In addition, specs do not compose, do they ? If you call a function g in a function f, there is no automatic check that their specs align.
> Types (in a reasonable static type system, not, say, C) are never wrong.
Oh man. This is the fundamental disagreement. Sure, you can have a type system that is never wrong in its own little world. But, that's not the problem. A lot of us are making a living mapping real world problems into software solutions. If that mapping is messed up (and it always is to some degree) then the formal correctness of the type system doesn't matter at all. It's like you got the wrong answer really, really right.
> A lot of us are making a living mapping real world problems into software solutions. If that mapping is messed up (and it always is to some degree) then the formal correctness of the type system doesn't matter at all.
If I'm understanding you correctly, you're saying statically typed language can't protect against design flaws, only implementation flaws. But implementation flaws are common, and statically typed languages do help to avoid those.
I'm not saying types always model your problem properly! That's not even well specified. I'm saying that "x has type foo" is never wrong if the program typechecks properly. That's totally different, and it means that you can rely on type annotations as being correct, up-to-date documentation. You can also trust that functions are never applied to the wrong number of arguments, or the wrong types; my point is that this guarantees more, in a less expressive way, than specs.
The words "static" or "analysis" do not appear there. I imagine you meant that you can runtime check the specs, which, well, is no replacement for a type system.
> Except, of course, that specs are only tested correct, not proven correct like types would be.
Yes this is the fundamental tradeoff. Specs et al are undoubtedly more flexible and expressive than static type systems, at the expense of some configurable error tolerance. I don't think one approach is generally better than the other, it's a question of tradeoffs between constraint complexity and confidence bounds.
Yeah, and I think this is obvious, but it certainly depends on the origin of the data being checked. We can prove the structure of “allowed” data ahead of time if we want guarantees on what’s possible inside our program. We also want a facility to check data encountered by the running program (i.e. from the user or another program.) which of course we can’t know ahead of time.
It is a design decision to be able to build a clojure system interactively while it is running, so a runtime type checker is a way for the developer to give up the safety of type constraints for this purpose—by using the same facility we already need in the real world, a way to check the structure of data we can’t anticipate.
Yes, I think that is one of the big weaknesses of it. You can write specs that make no sense and it will just let you. So far there is also no way to automatically check whether you are strengthening a guarantee or weaken your assumptions relative to a previous spec. In a perfect world we would have this in my opinion.
Not only that, Smalltalk and Lisps are languages designed with developer experience as part of the language.
You just don't get an interpreter/compiler and have to sort everything else by yourself, no, there is a full stack experience and development environment.
Well it does include that kind of behaviour but it's quite a bit more than just that. E.g. you could express something like "the parameter must be a date within the next 5 business days" - there's no static restriction. I'm not necesarily saying you should but just to give an illustrative example that there's less restrictions on your freedom to express what you need than in a static system.
>> types are isomorphic with schemas
I don't think that's a good way to think of this, you're imagining a rigid 1:1 tie of data and spec yet i could swap out your spec for my spec so that would be 1:n but those specs may make sense to compose in other data use cases so really it's m:n rather than 1:1
> E.g. you could express something like "the parameter must be a date within the next 5 business days" - there's no static restriction
Hm, I don't follow. If I were to write this in F#, there would be a type `Within5BusinessDays` with a private constructor that exposes one function/method `tryCreate` which returns a discriminated union: either an `Ok` of the `Within5BusinessDays` type, or an `Error` type with some error message. Once I have the type, I can then compose it with whatever and send it wherever and since F# records are immutable, I won't have to worry about invariants not holding. And since it's a type, I have the compiler/type system on my side to help with correctness.
(Side note, this is a bad example since the type can become invalid after literally 1 second... but since Clojure has the same problem I'm just running with it.)
I'm still learning Clojure (only a few months into it), but if I were to to write a spec, I'd have to specify what to do do if the spec failed to conform - same as returning the `Error` case in F#.
> i could swap out your spec for my spec so that would be 1:n but those specs may make sense to compose in other data use cases so really it's m:n rather than 1:1
Sorry, but I'm still not following - I believe you can do the same with types, especially if the type system support generics.
> If I were to write this in F#, there would be a type `Within5BusinessDays`
That’s not really the same thing - it’sa valid alternative approach but you’ve lost the benefits of a simple date - from (de)serialisation to the rich support for simple date types in libraries and other functions, the simple at-a-glance understanding that future readers could enjoy. Now the concept of date has been complected with some other niche concern.
> the type can become invalid after literally 1 second
Every system I’ve ever seen that has the concept of a business date strictly doesn’t derive it from wall clock date. E.g. it’s common that business date would be rolled at some convenient time (and most often not midnight) so you’d be free to ensure no impacts possible from the date roll.
>> I believe you can do the same with types, especially if the type system support generics
You can do something similar but you’ll need to change the system’s code.
It would be almost like gradual typing, except you could further choose to turn it off or to substitute your own types / schema without making changes to the system / code.
It’s quite a lot more flexible.
(Apols for slow reply - i 1up’d your reply earlier when i saw it but couldn’t reply then)
Right! I made the dumb, typical error to write: "You simply do not have that in a static world." When I should have written: "This type of expressiveness is not available in mainstream statically typed languages".
With "freely composable" I mean that you can program with these schemas as they are just data structures and you only specify the things you want to specify. Both advantage and the disadvantage is that this is dynamic.
You mean implicit type conversions? That's a thing you can get somewhat used to. But it throws off beginners and can introduce super weird bugs, because they hide bugs in weird ways, even if you are more experienced. Yes, I find strong typing strictly better than weak typing.
An even better example of this would be Excel, the horror stories are almost incredible.
So even if your environment is dynamic, you want clarity when you made a mistake. Handling errors gracefully and hiding them are very different things. The optimal in a dynamic world is to facilitate reasoning while not restricting expression.
It's always worth reminding folks that weak typing and implicit conversions can plague statically typed languages. C's implicit pointer array-to-pointer and pointer-type conversions are a major source of bugs for beginner and experienced programmers alike.
This is a consequence of weak typing rather than dynamic typing. I appreciate that these are not precise terms, but being able to change something's type (dynamic) is different to the language just doing strange things when you combine types (weak).
I've admittedly not played with spec, but can't you solve documenting interfaces by defining `defrecord`s ? You rarely really care about the actual types involved. You just want to know which fields you either need to provide or will recieve
Spec will give you stronger feedback than a docstring or function signature. It can tell you (in code terms, with a testable predicate) if a call to an interface wouldn't make sense.
Eg, spec can warn you when an argument doesn't make sense relative to the value of a second argument. Eg, with something like (modify-inventory {:shoes 2} :shoes -3) spec could pick up that you are about to subtract 3 from 2 and have negative shoes (impossible!) well before the function is called - so you can test elsewhere in the code using spec without having to call modify-inventory or implement specialist checking methods. And a library author can pass that information up the chain without clear English documentation and using only standard parts of the language.
You can't do that with defrecord, but it is effectively a form of documentation about how the arguments interact.
> Eg, spec can warn you when an argument doesn't make sense relative to the value of a second argument.
That's something that dependently-typed typesystems easily do as well if not better because inside of the function or data definition, the information is still available and used for code-completion, other compiletime checks etc.
There is very little spec logic. It looks a lot like type declarations in typed languages.
It's usually outside the scope of functions, since you are likely going to want to reuse those declarations. For example, you can use spec to generate test cases for something like quick-check.
You can add pre and post conditions to clojure function's metadata that test wether the spec complies with the function's input/output.
Maybe I've just never given it a chance, but I've never understood the appeal of being able to modify code in-memory while it's running.
I like a REPL for testing things out, or for doing quick one-off computations, but that's it. I would never want to, say, redefine a function in memory "while the code is running". Not just because of ergonomics, but because if I decide to keep that change, I now have to track down the code I typed in and manually copy it back over into my source files (assuming I can still find it at all). And if I make a series of changes over a session, the environment potentially gets more and more diverged from what's in sourced if I forget to copy any changes over. So I'd often want to re-load from scratch anyway, at least before I commit.
Am I missing something? Am I misunderstanding what people mean when they talk about coding from a REPL?
You can get an approximate taste of what it's like in plain old Java + JRebel, it's seriously about the same as trying to do it with Python + something like Flask. Start up a big application server in debug mode with Eclipse/IntelliJ, be annoyed that it takes 2+ minutes to start after everything's compiled. Now you want to work on a story, or some bug. You can make changes, save, it recompiles just that file (incremental compilation is a godsend in itself), and hotswaps it into the running application memory. No need to restart anything (with JRebel, for most common kinds of changes; without JRebel, only for some changes). It's particularly useful when you're debugging, you find the problem, change the code, and re-execute immediately to verify to yourself it's fixed.
You also can get an experience like PHP, where you just have to change and save some files, and your subsequent requests will use the new code. This is so much better than shutting down everything and restarting and is a large part of why CGI workflows dominated the web.
Common Lisp takes these experiences and dials them to 11, the whole language is built to reinforce the development style of dynamic changes, rather than an after-thought that requires a huge IDE+proprietary java agent. It's still best to use some sort of editor or IDE, and then you don't have any worry about source de-syncs -- frequently you'll make multiple changes across multiple files and then just reload the whole module and any files that changed with one function call, which you might bind to an editor shortcut, but crucially like debugging is not centrally a feature of the editor but the language; the language's plain REPL by itself is just a lot more supportive of interactive development than Python/JS/Ruby's. Clojure, and I personally think even Java with the appropriate tools, are between Python and CL for niceness of interactive development, but Clojure tends to be better than the Java IDE experience because of its other focus on immutability.
As others have mentioned - you’re probably talking about something like a python or node repl. Lisp repl development is not like python or node - you _work_ in the repl. The closer comparison might be between bash+node as a repl - up+enter and the like to rerun tests has an equivalent in clojure+IntelliJ. There’s no copy pasting but there are different key bindings.
One of the best parts about lisp style repl development is that you end up doing TDD automatically. You just redefine a function until it does what you want from sample data you pass in - without changing files or remembering how your test framework works. You can save the output of some http call in a top level variable and iterate on the code to process it into something useful. The code you evaluate lives in the file that will eventually house it anyway so it’s pretty common to just eval the entire file instead of just one function.
Since you don’t ever shut the repl down, developing huge apps is also quite pleasant. You only reload the code that you’re changing - not the rest of the infra so things like “memoize” can work in local development. That’s why it’s a bit closer to your bash shell in other languages.
If you’ve never tried it, I highly recommend trying the Clojure reloaded workflow [1] to build a web app with a db connection. You can really get into a flow building stuff instead of waiting for your migrations to run on every test boot.
Yes, what you are missing is that usually you are using an editor like emacs where you can modify a specific form and send it to the repl. That way there is no chasing back through repl history for a form to paste back into a file.
> These can be gained back with clojure spec and other fantastic libraries like schema and malli. What you get here goes way beyond what a strict, static type systems gets you
That reads like what someone would think when they used the typesystem of Java 6 and now think that this is what "statically typed programming" means.
No, you can _not_ get back what types give you by any kind of spec - if anything you can get some of the benefits of types, but you also pay a price.
The thing is - dynamically typed languages don't really seem to evolve anymore. They add a bit of syntatic sugar here and there and sometimes add some cool feature, but mostly only features that already existed for a long time in other languages. At least that is what I have seen over the past couple of years, I would be happy to be proven wrong.
Looking at statical typesystems however, there is much more progress, simply because they are much more complex and not as optimized. From row-types over implicits and context-expressions towards fully fledged value-dependent typesystems, which have amazing features that start to slowly trickle down into mainstream languages like Typescript or Scala.
While both dynamically and statically typed languages have their pros and cons and it will stay like that forever, I expect that statically typed languages will proceed to become the bigger and bigger piece of the cake, simply because they have more potential for optimizations going forward.
I keep seeing lisp people bandy about all of this design by contract/arbitrary predicate validation stuff. Can you give an example of an instance in which static types + runtime checks don't completely subsume this?
My intuition is that almost all of these methods people are talking about would have to be enforced at run-time, in which case I don't see how it's providing anything fundamentally more than writing an assertion or a conditional.
They don't make these impossible, they typically just don't let you express these within the type system and they typically don't let you not specify your types.
I should have made clear that I'm emphasizing the advantages of being dynamic to describe and check the shape of your data to the degree of your choosing. Static typing is very powerful and useful, but writing dynamic code interactively is not just "woopdiedoo" is kind of the point I wanted to make without being overzealous/ignorant.
That largely depends on the type system. Languages like Haskell and Scala which have much more powerful type systems than C/Java/Go/etc absolutely do allow you to do those sorts of things. It is a bit harder to wrap your head around to be sure and there are some rough edges, but once you get the hang of it you can get the benefits of static typing with the flexibility of dynamic typing. See https://github.com/milessabin/shapeless or a project that I've been working on a lot lately https://github.com/zio/zio-schema.
I feel like there's a missing axis in the static/dynamic debate: the language's information model.
In an OOP language, types are hugely important, because the types let you know the object's ad-hoc API. OOP types are incredibly complicated.
In lisps, and Clojure in particular, your information model is scalars, lists, and maps. These are fully generic structures whose API is the standard Clojure lib. This means that its both far easier to keep the flow of data through your program in your head.
This gives you a 2x2 matrix to sort languages into, static vs dynamic, and OOP vs value based.
* OOP x static works thanks to awesome IDE tooling enabled by static typing
* value x static works due to powerful type systems
* value x dynamic works due to powerful generic APIs
* OOP x dynamic is a dumpster fire of trying to figure out what object you're dealing with at any given time (looking right at you Python and Ruby)
CLOS is wierd since it sits in a multiparadigm language, so CL really spans boxes. JS is the same as well depending on how you use it. Smalltalk is a great example since it really did manage to do OOP x dynamic in a much better way and its worth wondering why Smalltalk pulled it off where Ruby feels like a nightmare. I suspect it has to do with focusing on the message passing aspect.
So no, it's not a perfect model, but I think its more informative than looking at languages on a one dimensional static / dynamic axis.
One thing I don't like about all articles on clojure is that basically all of them say: ah, it's just like lisp with lists `(an (example of) (a list))` with vectors `[1 2 3]` thrown in. So easy!
But then you get to Clojure proper, and you run into additional syntax that either convention or functions/macros that look like additional syntax.
Ok, granted, -> and ->> are easy to reason about (though they look like additional syntax).
But then there's entirely ungooglable ^ that I see in code from time to time. Or the convention (?) that call methods on Java code (?) with a `.-`
Or atoms defined with @ and dereferenced with *
Or the { :key value } structure
There's way more syntax (or things that can be perceived as syntax, especially to beginners) in Clojure than the articles pretend there is.
(defn ^:export db_with [db entities]
(d/db-with db (entities->clj entities)))
(defn entity-db
"Returns a db that entity was created from."
[^Entity entity]
{:pre [(de/entity? entity)]}
(.-db entity))
(defn ^:after-load ^:export refresh []
(let [mount (js/document.querySelector ".mount")
comp (if (editor.debug/debug?)
(editor.debug/ui editor)
(do
(when (nil? @*post)
(reset! *post (-> (.getAttribute mount "data") (edn/read-string))))
(editor *post)))]
(rum/mount comp mount)))
You missed it, it has been there forever. But it says good and bad things about Clojure that its reference documentation is one of its weakest points.
The Guide/Reference split obscures a lot of information (do I want guidance on Deps & CLI or do I want reference on Deps & CLI?) and the guides where that gem is hidden randomly mix advanced topics (eg, how to set up generative testing), beginner topics (how to read Clojure code) and library author topics (eg, Reader Conditionals).
When you think about it, there is nearly no trigger to look at the guides when the information you need is there. Clojure is a weird mix of both well documented and terribly documented. All the facts are on the website, very few of them are accessible when required. The people who make it past that gauntlet are rewarded by getting to use Clojure.
is calling the method `.getAttribute` on the `mount` object – since it's a Lisp, it's in prefix notation. It also highlights how methods are not special and just functions that receive the object as first argument.
Finally,
@*post
is the same as
(deref *post)
and the `*` means nothing to the language – any character is valid on symbol names, the author just chose an asterisk.
Most of what you believe to be syntax are convenience "reader macros" (https://clojure.org/reference/reader), and you can extend with your own. You can write the same code without any of it, but then you'll have more "redundant" parenthesis.
Minor point of order about the atoms: they're not defined with @ nor derefd with . If you're referring to earmuffs* that's convention not syntax (specifically for dynamically scoped variables, which could be atoms or anything else), and @ is indeed deref. (More specifically @x is a reader macro ish that expands to literally `(deref x)`.)
Single engineers will pick clojure at companies , build a project in it, later that engineer will move on, now nobody can maintain this code so it’s rewritten in some normal language. I’ve seen that happen a few times. That code is hard to read and understand. This is why clojure will remain niche.
You need a team that wants to use Clojure. I wrote Clojure professionally for 2 years, and everyone at the company was excited about it and sold on the language. Even after 3-5 years of programming in it. Now, at a different place, we write in a different language, and even though I still love Clojure, I'm not gonna write some project in it, even if Clojure might suit it so well, because I know these people are sold on different language, and I'm not going to preach and I'm not going to make their lives more difficult by having to maintain some obscure codebase.
You've seen a case where someone wrote something in Python that later devs could not understand and then rewrote it in . . . what? And you've seen that with Java?
There's a big difference between a developer going off and writing something in one of the top five most used languages in the world and doing so in Scala.
1. picking a language/tool that a company doesn't have personnel with experience using it
2. picking a language/tool that is esoteric, which generally implies #1 as well.
#1 on its own isn't great, but generally when sticking in the java/python/ruby/javascript/php/etc...mainstream languages, there's a lot more documentation, and there's a higher chance that _someone_ in the company will have some familiarity. If nothing else, it'd be easier to hire a replacement for.
A higher chance, yes, but it doesn't matter much; what is tricky with most applications is the domain. Certainly, it's faster to go learn a language than to learn a new domain. To that end, you can get the whole team trained faster in a language than you can hire someone with experience and train them to the domain.
> Certainly, it's faster to go learn a language than to learn a new domain.
It's not only the language but the framework. For example I know javascript well enough but I now am quite a noob with Ember in my new role.
I would say the framework is just as important as the language, at least when doing web development.
You're kind of reinforcing the point though -- now you've got a whole team distracted by picking up a new language....why? how is it a good use of anyone's time? And it'll be a perennial training issue in the case of an esoteric language, because those team members will eventually turn over as well, meaning that you don't get to avoid either hiring or training a new person on it.
If it's just one component, implemented by a single dev, it really can make more sense to understand what it does and rewrite it in a language that's common in the company.
I'm not advocating NOT rewriting it. I'm just saying, back to the great grandparent's point, that the issue is a dev went rogue, NOT the language the rogue dev chose. The difficulty is the same regardless of the language the rogue dev chose; it's not that they picked Clojure, it's that they picked a language there was no organizational adoption of.
Yes. I've seen and contributed to dumpster fires in all of those languages. I would love to say it was all some rogue developer that crapped on things, but it is often just new developers. The more, the more damage.
It could have been Go and Java programmer trying to understand it. Or it could have been some clumsy tool written in node which Go programmer finds hard to read and understand. Clojure's main advantage is that you can you can learn it very very quickly up to the point when you understand most of the code, the language is very very small compared to "five main languages".
> Single engineers will pick clojure at companies , build a project in it, later that engineer will move on, now nobody can maintain this code so it’s rewritten in some normal language
"Normal language"?
You mean, whatever language is most popular at the company. What's "normal" at one would be completely alien at another. Even things like Java. If you don't have anything in the Java ecosystem, the oddball Java app will be alien and will likely get rewritten into something else.
The reason Clojure remains niche is that some people somehow think it's not a "normal" language, for whatever reason.
is it really hard to read (could be) or is it just that the average coder never saw lisp or sml and doesn't want to bother bearing the responsibility to learn something alien on duty ?
Agreed. These days I'm really fascinated by clojure and trying to learn clojure. Other than the project setup and repl and the editor (which I had considered), these weird characters are throwing me off.
What clojure really needs is some kind of opinionated framework or starter template, something like create-react-app. That has all these things figured out so a beginner like me can start playing with actual clojure, which documents all the steps to setup the repl and editor and what not. The last time I asked for this I was told about lein templates, they help but there's no documentation to go with those.
There needs to be some push from the top level. create-react-app was produced by facebook. Elm reactor (which lets you just create a .elm file and play with elm) was created by Evan the language creator himself.
tldr: There's a huge barrier to start playing with clojure that needs to come down and the push needs happen from the top level.
Yes, of course and I've got the book as well. The problem with the book is I got stuck on the very first code example in the book. I know there's a forum for the book where (hopefully) I can get my query answered.
My point is: these are all individual attempts (the book i mean) and there will always be something on page xyz broken and it can't be solved by individuals. To solve these problems, there needs to be constant time and money investment from someone serious (like facebook in case of create-elm-app).
Yes I agree there is a problem of a lack of institutional funding in the Clojure world. Luminus is a great tool but it is a bit sad that it is arguably the most production-ready web toolkit in the ecosystem and it is mostly the work of a single person.
There is some community effort to better fund the core infrastructure in Clojure through https://www.clojuriststogether.org/, hopefully they can continue to attract more funding developers and companies.
In general a lot of these issues could be alleviated if the community was just in general larger with more contributors. I think the Clojure community is quite welcoming to newbies in the sense that people are quite responsive, kind and helpful around the internet, in Clojurians Slack (try asking there btw, if you haven't yet and are still stuck at the start of the book), etc. But in other ways people seem averse to criticism or suggestions from outsiders. I think the Clojure world needs to do a bit of self reflection to understand why adoption is so low right now and honestly consider what needs to change to attract more developers and contributors.
> An incoming HTTP request? it is a plain Clojure dictionary.
I learned to code in Python. Loved it. Dynamically typed dicts up the wazoo!
Then I learned why I prefer actual types. Because then when I read code, I don't have to read the code that populates the dicts to understand what fields exist.
The two are not mutually exclusive. Clojure has namespaced keywords and specs[0] to cover that. (There is also the third-party malli, which takes a slightly different appproach.)
The advantage is that maps are extensible. So, you can have middleware that e.g. checks authentication and authorization, adds keys to the map, that later code can check it directly. Namespacing guarantees nobody stomps on anyone else's feet. Spec/malli and friends tell you what to expect at those keys. You can sort of do the same thing in some other programming languages, but generally you're missing one of 1) typechecking 2) namespacing 3) convenience.
This is one of those self-inflicted Clojure problems. In Common Lisp you might use an alist or a plist for small things, but you'd definitely reach for CLOS classes for things that had relationships to other things and things that had greater complexity.
IIRC, the preference for complecting things via maps, and then beating back the hordes of problems with that via clojure.spec.alpha (alpha2?) is a Hickey preference. I don't recall exactly why.
No source to back this up, but my guess is that Clojure was driven by the need to interopt with Java so is to not get kicked out of production. This meant absorbing the Java object model. Shipping a language with both Java objects and CLOS and making them both play nice together sounds like a nightmare.
There's a Common Lisp implementation on the JVM, called ABCL: https://www.abcl.org/ The interop is... not the best, but it's something. I've only used it for proof-of-concept stuff (e.g. how-to make a Lisp module, export it as a jar that java code can include in their pom and use without knowing it's Lisp) and for minor development experience enhancements in a giant Java codebase (e.g. change method in Java, it gets hot-swapped in, I invoke it or an upstream method from Lisp with real data so I don't have to make an even higher upstream network request via some deep UI section).
This comment helpfully explains many of the reasons Rich had for choosing immutable, persistent, generic data structures as the core information model in clojure (instead of concrete objects / classes): https://news.ycombinator.com/item?id=28041219
Not wanting to misquote the above / Rich himself I would TLDR it to:
- flexibility of data manipulation
- resilience in the face of a changing outside world
- ease of handling partial data or a changing subset of data as it flows through your program
Please note that no one (I hope) is saying that the above things are impossible or even necessarily difficult with static typing / OOP. However myself and other clojurists at least find the tradeoff of dynamic typing + generic maps in clojure to be a net positive especially when doing information heavy programming (e.g. most business applications)
Namedtuples FTW! A de-facto immutable dict with the keys listed right there in the definition to obviate all the usage head-scratching. Then, if you need more functionality (eg factory functions to fill in sensible defaults), you can just subclass it.
TBH I've never understood the attraction of the untyped dict beyond simple one-off hackups (and even there namedtuples are preferable), because like you say you typically have no idea what's supposed to be in there.
Question: 1. Can a GET request have a non-empty request body?
2. Assuming you don’t know the answer to that question, will the type system you use be able to tell you the answer to that question?
This is a pretty simple constraint one might want (a constraint that only certain requests have a body) but already a lot of static type systems (e.g. the C type system) cannot express and check it. If you can express that constraint, is it still easy to have a single function to inspect headers on any request? What about changing that constraint in the type system when you reread the spec? Is it easy?
The point isn’t that type systems are pointless but that they are different and one should focus on what the type system can do for you, and at what cost.
Any statically-typed language with generics can express that by parameterising the request type with the body type. A bodiless request is then just Request[Nothing] (or Request[Unit] if your type system doesn't have a bottom type). Accessing the headers just requires an interface which all static languages should be able to express.
(1) note that “statically-typed language with generics” excludes a lot of statically typed languages, including C and Go (at least pre generics).
(2) this misses the meat of the question which is how to express that (eg) a GET request doesn’t come with a body and a POST request does. I suppose that you’re suggesting that one registers a url handler with a method type and that forces the handler to accept responses of a certain type. Or perhaps you are implicitly allowing for sun types (which aren’t a thing in many static type systems.)
(3) even in C++, isn’t this suggestion hard to work with. That is, isn’t it annoying to write a program which works for any request whether or not it has a body because the type of the body must be a template parameter that adds templates to the type of every method which is generic to it. But maybe that is ok or I just don’t understand C++.
1) Looking at the TIOBE index, all the static languages I recognised on there are: C,C++,C#,Visual Basic,Go,Fortran,Swift,Delphi,Cobol,Rust,Scala,Typescript,Kotlin,Haskell and D. Of these C and Go are the only two that don't appear to support generics so I don't think this approach excludes a lot of static languages.
2) If you want to distinguish GET and POST requests statically then you just need a type for them e.g.
GetRequest<TBody> implements Request<TBody> { }
if you don't need to do this then you can just add a method field and use a single type for both. Either way you don't need to use sum types so a language like Java can express it.
3) Yes you'll have to make functions that don't care about the body type generic so this approach could become unwieldy if you have a few such properties you want to track.
F# has a feature called type providers that make this sort of bookkeeping between the database and the code less tedious, but even if you mess it up, static typing still gives you more safety than dynamic. If your code blew up because it should have accepted an identifier it didn’t, you know that the code has not been written to handle that case and can fix it. Alternatively, you can just choose to ignore this, and do what a dynamic language does. There is nothing stopping you from being dynamic in a static language, passing everything around as a map, etc.
Does “the request type has a body property” actually imply (1) though? In a language like C or C++ or Java, you could have a protocol like “body is always null on GET requests.” The question isn’t really about HTTP, that was just an easy-to-reach-for example, it is really about what having explicit types allows one to deduce about a program.
To be fair, an incoming request is, almost by definition, dynamic. It makes sense to have that as a map, since the main sensible thing to do on receipt is validation/inspection.
Granted, you may have a framework do a fair bit of that. Depends how much you want between receipt of the request and code you directly control.
Usually the approach in a statically-typed language is to transform your dynamic request into something that you know through parsing instead of validation. Here's a great article about this: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va....
This is the second time I've seen the link above. And while I agree with the premise, the author clearly does not understand how to properly use the `Maybe` monad (a term that does not make an appearance!).
There is little use in wrapping a call in `Maybe` to then immediately unwrap the result on the next line. Doing so isn't really using the construct... One would expect the lines following the creation of `Maybe` to bind calls through the monad.
In the end I see almost no meaningful difference between their "Paying it forward" example and simply utilizing an `if` to check the result and throw. In essence the author is using a parse and validate approach!
You might try re-reading it with some charity - the example's purpose isn't to teach the `Maybe` monad, but to remove the redundant check. To go into what `bind` does would be a diversion from the main topic (parsing vs validating).
But `Maybe` is specifically designed to remove redundant checks for a value that may (or may not) be present! That's the whole point of the monad! It seems rather unfortunate this isn't highlighted (or at least illustrated) doesn't it?
I think you're referring to this part of the `getConfigurationDirectories` action, which has type `IO (NonEmpty FilePath)`:
case nonEmpty configDirsList of
Just nonEmptyConfigDirsList -> pure nonEmptyConfigDirsList
Nothing -> throwIO $ userError "CONFIG_DIRS cannot be empty"
The "meaningful difference" you're looking for is the type of `getConfigurationDirectories`. The previous version had type `IO [FilePath] `, which _doesn't_ guarantee any configuration directories at all. It did indeed check the results and throw. But it doesn't guarantee that all the `[FilePath]` values in the program have been checked. There are neither tests nor proofs in this code. In contrast, with the revised version, you can be certain anywhere you see a `NonEmpty FilePath` it is indeed non-empty.
The code I've quoted that checks which case we have, is the only place that needs to handle that `Maybe`. Or maybe `main`, if we want to be more graceful. The author (I wouldn't say I know her but I know that much) does know how to chain maybes with bind but it's not necessary in this example code.
My point is that if you are not chaining `Maybe` then the utility of employing the construct is unobserved. The entire purpose of using `Maybe` is to relieve the client from the need to make checks at every call for a value that may (or may not) exist. If you intend to immediately "break out" of the monad and (even more specifically) throw an error, you might as well just use an `if`.
I'm sure `main` could be written to "bind"/"map" `getConfigurationDirectories` with `nonEmpty`, `head`, and `initializeCache` in a way that puts the `throw` at the top-level (of course the above implementations may need to change as well). Unfortunately I'm not familiar enough with Haskell to illustrate it myself.
The purpose of Maybe is to explicitly represent the possible non-existence of a value which in Haskell is the only option since there's no null value which inhabits every type. The existence of the monad instance is convenient but it's not fundamental. The type of getConfigurationDirectories could be changed to MaybeT IO (NonEmpty FilePath) to avoid the match but I don't think it would make such a small example clearer.
There are numerous ways to redesign the function signatures, but I would imagine the simplest would be (again, idk Haskell syntax):
getConfigurationDirectories: unit -> Maybe [FilePath]
nonEmpty: [a] -> Maybe [a]
head: [a] -> Maybe a
initializeCache: FilePath -> unit
Notice `nonEmpty` isn't really necessary because `head` could to the work. The above could be chained into a single, cohesive stack of calls where the result of each is piped through the appropriate `Maybe` method into the next call in a point-free style. I cannot imagine how this wouldn't be clearer. e.g:
maybeInitialized <- (getCofigurationDirectories >>= head >> initializeCache)
That's the whole thing. Crystal clear. The big takeaway of "Parse don't validate" should be about the predominant use of the `Maybe` monad as a construct to make "parsing" as ergonomic as possible! Each function that returns `Maybe` can be understood as a "parser" that, of course, can be elegantly combined to achieve your result.
My critique is exactly that unwrapping the `Maybe` immediately in order to throw an exception is kind of the worst of both worlds. I mentioned this in a sibling comment, but my sense is that the author is more concerned with have a concrete value (`configDirs`) available in the scope of `main` than best-representing the solution to the problem in code. It is a shame because I agree with the thesis.
On the contrary the The NonEmpty type is fundamental to the approach in that example since it contains in the type the property being checked dynamically (that the list is non-empty). The nonEmpty function is a simple example of the 'parse don't validate' approach since it goes from a broader to a more restricted type, along with the possibility of failure if the constraint was not satisfied. The restriction on the NonEmpty type is what allows NonEmpty.head to return an a instead of a (Maybe a) and thus avoid the redundant check in the second example. The nonEmpty in your alternative implementation is only validating not parsing since after checking the input list is non-empty, it immediately discards the information in the return type. This forces the user to deal with a Nothing result from head that can never happen. Attempting to clean the code up by propagating Nothing values using bind is just hiding the problem that the validating approach avoids entirely.
You are misunderstanding the system. You can organize the logic into whatever containers you want, but the essence of the system cannot be changed.
You are already handling a `Maybe` type because it's possible for your input to not exist. Because the first implementation of `head` also returns a `Maybe`, it is possible to "bind" them together (I'm leaving out `IO` because I am both unsure of the syntax[0] and it is immaterial to the example):
head :: [a] -> Maybe a
head (x:_) = Just x
head [] = Nothing
getConfDirs :: Maybe [FilePath]
initializeCache :: FilePath -> Cache
useCache:: Cache -> Value
main :: ()
main = do
// you don't need concrete values here
maybeCache <- (getCofDirs >>= head >> initializeCache) // Maybe Cache
// one option
case maybeCache of
Just c -> useCache c
Nothing -> error "CONFIG_DIRS cannot be empty"
// another option
maybeValue <- (maybeCache >> useCache) // Maybe Value
[0] I have never written Haskell, so the above is my best-guess at the syntax given the snippets available (and no extra research)
The two functions `head` and `getConfDirs` are "parsers" because they both return `Maybe`. Contrary to
> Returning Maybe is undoubtably convenient when we’re implementing head. However, it becomes significantly less convenient when we want to actually use it!
It is trivial to use a reference to `Maybe` because it is a monad that it is specifically designed to be used more conveniently than the alternative approaches in the case when a value may (or may not) exist.
maybeCache <- (getCofDirs >>= head >> initializeCache)
is doing exactly what the post is arguing against. getConfDirs is validating the list is non-empty but the [FilePath] list it contains does not encode that information. Now you immediately have to handle the possibility of a missing value from head that you already know cannot happen. This isn't too apparent here since you've combined it into a single expression but if you need to pass the confDirs list to any other part of the program they will also have to continually handle the possibility of the list being empty even though you already checked for that possibility. Now every function that interects with the confDirs list will have to include (Maybe a) in its return type unnecessarily. The post is not suggesting you can remove Maybe entirely but it has moved it to a single point in the program (the point where the config dirs list is checked for emptiness) and removed it everywhere else. Your approach must continually guard against an impossible condition everywhere the dirs list is accessed because you discard the property you checked for in getConfDirs.
The monadic operators make it convenient to propagate missing values through a chain of operations but they are not the primary benefit of an explicit Maybe type. Much like IO, the benefit of having an explicit Maybe type is when you _don't_ have it since its absence represents more information at that point in the program. Likewise a (NonEmpty a) contains more informatation than [a] which consequently makes the implementation of head more informative.
The parsers in this approach have types like
a -> Maybe b
where type b contains the extra information extracted by the parser. Your getConfDirs function only contains a function with type
I understand what the author is doing. I said this earlier but it bears repeating, the author seems to be more concerned with having a concrete type than simpler code. A reference to `Maybe Cache` is good enough (and preferred). The top-level of your program is precisely where you want to have the flexibility to deal with the above.
Furthermore, my example is a much better illustration of the axiom ("Parse don't validate") than what the author is doing -- which is more like "Parse and validate".
You need to clarify "continuously guard". Sure you have to invoke methods like:
maybeCache >> useCache // map
instead of:
maybeCache |> useCache // not sure how Haskell pipes
Is that too difficult? The `Maybe` monad is specifically designed so that you don't have to continuously guard against the possibility of the value not existing. That is, you can "map", "bind" and "apply" functions to the value as if it always exists (and it handles the situation when the value doesn't). I also included a `case` block within which you can be statically certain a value of type `Cache` is available if you really need it.
The purpose of `Maybe` is to simplify code that needs to deal with a value that might not exist. Attempting to organize your code to avoid using `Maybe` is, by definition, going to be more cumbersome than simply leaning into the construct (that's what it's for!). It also better-illustrates how "parse don't validate" should work. Using an exception to guard against an invariant is... validating not parsing.
You don't need to defend the author here. It's just a matter of fact the the code provided could be organized differently according to a more idiomatic usage of `Maybe`, and therefore a more illustrative example of their own point. The choice to exemplify something else is unfortunate and the thrust of this entire comment thread -- I felt like I had to say something now seeing that link a second time.
The author explains what they mean by parsing in the post:
> Really, a parser is just a function that consumes less-structured input and produces more-structured output. By its very nature, a parser is a partial function—some values in the domain do not correspond to any value in the range—so all parsers must have some notion of failure. Often, the input to a parser is text, but this is by no means a requirement, and parseNonEmpty is a perfectly cromulent parser: it parses lists into non-empty lists, signaling failure by terminating the program with an error message.
So the properties checked by the parser are reflected in the output type. Reifying these properties in the type is what allows the validation to be done once at the top level and avoided throughout the rest of the program. Your complaint about throwing exceptions is focusing on an irrelevant detail in a small example - yes this could have been moved into the main function but doesn't affect the overall behaviour.
However your argument that propagating Maybe values is more idiomatic than parsing into a more precise type is one I - and I assume most - static typing advocates would disagree with. Given the choice you would always prefer an 'a' over a 'Maybe a' since a Maybe represents a point of uncertainty which you would rather not have. As a result, having to chain this imprecision using various combinators is inherently more complex than not having to do so. Yes, using bind etc. is preferable to manually destructing Maybe values but avoiding Maybe is more preferable still.
You can't avoid `Maybe` in this system. It is in the nature of the problem (as it is designed) that the input might not exist (and therefore a list might be empty). The question isn't one of avoidance, rather, integration. How do we deal with problems like the example?
"Parse don't validate" is a great way to deal with it! Even more convenient is the existence of a tool that can be used to offload all of the redundancy involved when choosing to parse instead of validate (i.e. throw an error).
It is the author's prerogative to value having a concrete value at one specific point in the program (`main`) over demonstrating how using `Maybe` can make parsing a breeze. Clearly you also value (for whatever reason) knowing that a variable contains a value at some specific, rather arbitrary point in the example program[0]. But it is an unfortunate choice given the title of the post.
Not only does the example code in the post not illustrate "parse don't validate" very well, it convolutes the solution considerably. My example above is able to achieve identical behavior in an easier-to-digest flow while also illustrating how parsing instead of validating can be done.
[0] Of course we know that any function to which we `map` to our `maybeCache` will for sure be invoked with an instance of `Cache`.
Your example does not achieve idential behaviour at all since it 'parses' an [a] to another [a] and therefore throws away the very property you've just checked. The (NonEmpty a) property encodes the non-emptiness of the list in the type which is then known at every point the list is accessed throughout the entire rest of the program. The point is not just to check the non-emptiness in main as you appear to be implying. Any use of head on an [a] must continually deal with a (Maybe a) even though this possibility has been ruled out. In contrast NonEmpty.head returns an element directly so removes entires chains of Maybes that would be propagated, conveniently with map/bind or otherwise. Parsing allows to replace N + 1 instances of Maybe with just 1 so you can't claim your approaches are the same just because it hasn't been eliminated entirely.
I think you are confusing implementation with behavior. That is, I am achieving that same result through different means. I am mostly uninterested in specifically how the configuration string is parsed. It's not really important.
What is important is that we know we will have to deal with the possibility of something not existing. That is where the complexity lies, and where we want to take care to make our program as sensible as possible. Validating your input to throw an exception or return is one way to satisfy the compiler, another way is to use `Maybe` as intended. The author's "solution" is simply a poor illustration of parsing over validation (read that sentence again).
I suspect, and this applies to you as well, that they are just not comfortable working with the `Maybe` construct. Adding extra ceremony to remove a `Maybe` is simply not worth the trouble, and your idea of "continuously propagating" is severely overblown. Again, we can write every single line of the rest of our program as if `Cache` exists. You don't need to "handle" anything extra (other than the holding the concept of a slightly more complex value in your mind).
The difference between parsing and validation in the author's formulation is not between returning Maybe and throwing an exception, it's between returning a more precise type and not. Here's the types of the two version of `getConfigurationDirectories`:
The second version is preferred because the (NonEmpty FilePath) encodes the property that was checked in the type which means it doesn't have to be handled repeatedly throughout the entire rest of the program.
Yes the second version could have been changed to one of:
but this would only have moved the error reporting up one level to the main function. I would guess the existing version was chosen to simplify the types for a non-Haskell audience.
is NOT an example of parsing because [FilePath] does not remove the possibility (in the types!) of the list being empty. When you later attempted to use
maybeCache >>= useCache
this requires the type of useCache to have type
[FilePath] -> IO a
for some output type a. This function must deal with the possibility of the input list being empty because the type allows it. Every call to `head` returns (Maybe FilePath) and must handle the Nothing case. Neither I nor the author is unaware that there are many combinators that make this more convenient than explicit matching against Just/Nothing but doing so is strictly worse than returning a FilePath directly. Presumably none of the lower-level functions will be able to provide a default FilePath to use so every single one will be forced to return a Maybe somewhere in their return type (or use fromJust which is very ugly). This affects every single one of their callees which will again be forced to propagate Maybe up to their callees etc. To reiterate: the issue is not the possible non-existence of Cache, which can be handled in main. It's that the representation of Cache forces every single operation on it (of which head is just one simple example) to potentially have to represent conditions that should not actually be possible. This is a failure to 'make invalid states unprepresentable', which most proponents of static types aspire to.
You have the signature for `useCache` wrong. I defined it above (`Cache -> a`). Notice the concrete type...
I cannot stress this enough. You do not need to remove the possibility of a value not existing in order to compose a simple, coherent program. This is because `Maybe` is designed to handle all of the extra ceremony involved with utilizing such values. You only need to use `>>` (map) instead of `|>` (pipe) when invoking your functions. That is it.
All of the above is really beside the point though, because I am not arguing that one way is necessarily better than the other. I am arguing that the author's post is titled "Parse don't validate", that the perfect construct is right there to exemplify how parsing unstructured data into/through a system can be done, but then the author eschews it in favor of... validation (with what appears to be some tricks to fool the compiler)!
If your guard against an invalid state is to throw an exception you are validating. Attempting to redefine the terms to fit a particular narrative is a distraction that serves no one.
> Neither I nor the author is unaware that there are many combinators that make this more convenient than explicit matching against Just/Nothing but doing so is strictly worse than returning a FilePath directly
I'd like you to define "strictly worse" here. In order for "strictly worse" to make any sense we would need to define "strictly better" to mean something like: "to have a reference to a variable in this particular scope that is definitely a `FilePath`". But why are variables in this scope (`main`) so important? You can get reference to a `FilePath` directly whenever you need it through a `Maybe`:
There is no difference in behavior and only a slight difference in implementation. I suppose if you really really wanted to `print` the value of `FilePath` from `main` (and not some other function), the second version would be preferred (though you could still match in the first version to create a block in main where `FilePath` is statically defined). Pretty arbitrary though.
> You have the signature for `useCache` wrong. I defined it above (`Cache -> a`)
Yes, sorry it's actually the line
maybeCache <- (getConfDirs >>= head >> initializeCache)
which shows the issue.
> but then the author eschews it in favor of... validation (with what appears to be some tricks to fool the compiler)!
I think the author is pretty clear about how they're using the terms 'validation' and 'parsing' in the post - validation functions do not return a useful value while parsers refine the input type and carry a notion of failure. The first two examples of parsers they give are:
nonEmpty :: [a] -> Maybe (NonEmpty a)
parseNonEmpty :: [a] -> IO (NonEmpty a)
you seem to be arguing that parseNonEmpty is validating because it throws an exception instead of returning Maybe (NonEmpty a) but this isn't true here since Maybe signals failure by returning Nothing errors within IO can be signaled with exceptions. The author hints at how these two parser types are related later on with:
checkNoDuplicateKeys :: (MonadError AppError m, Eq k) => [(k, v)] -> m ()
There are MonadError instances for both IO and Maybe so the general parser type is something like
MonadError e m => a -> m b
Admittedly this could have been made clearer if it was the intention and returning Maybe is preferable to throwing exceptions in languages like Haksell.
If you were translating this approach to other languages like Java or C# though you proabably would throw exceptions to indicate failure e.g.
interface Parser<A, B> { B parse(A input) throws ParseException; }
so I don't think your objection holds in general.
> I'd like you to define "strictly worse" here
I'm saying you would always prefer to be handed an instance of an `a` instead of a (Maybe a) since it's more precise. You can trivially construct a (Maybe a) from an a but you can't easily go in the other direction. You either need to produce a dfeault value or use a partial function like fromJust to obtain an 'a' from a 'Maybe a'. The motivation for the post is to show how using a more precise data type allow you to remove these from the rest of the code.
> But why are variables in this scope (`main`) so important
The issue doesn't happen in main, it happens throughout the rest of the program. The high-level structure is something like:
main :: IO ()
main = do
maybeDirs <- getConfigDirs
maybeDirs >>= restOfProgram
main only has to handle the parse failure and report any errors which will look similar regardless of whether getConfigDirs has type Maybe (NonEmpty FilePath) or IO (NonEmpty FilePath) (and throws an exception). But the representation of the directory list could be used anywhere in restOfProgram. Given a chain of applications fun1 -> fun2 -> ... -> funN, if funN accesses the file list with head and receives a (Maybe FilePath) there are three options:
1. Use fromJust since the list should be non-empty
2. Produce a default value
3. Propagate the Maybe in the return type of funN
Option 1 is messy, 2 is also unlikely for a low-level function and 3 forces fun1 to fun (N - 1) to either handle or propagate the partiality. Yes using >>= and <=< etc. can hide this plumbing but can be made unnecessary in the first place.
> I'm saying you would always prefer to be handed an instance of an `a` instead of a (Maybe a) since it's more precise.
I disagree with this. `Maybe a` is more precise because it more closely represents the actual system within which we are working. It is simply a fact that our configuration directories might not exist. It is only within the author's own head that they prefer a concrete type because they value being able to point to their variable and say, "look I have this value! It's right here!" in a procedural sense, more than adopting a more functional approach.
> You can trivially construct a (Maybe a) from an a but you can't easily go in the other direction. You either need to produce a dfeault value or use a partial function like fromJust to obtain an 'a' from a 'Maybe a'
Again, the above is just not accurate! Or it is accurate in a very specific - "I want this particular value in this particular scope" - kind of way. Even in your example, we can be statically certain that `restOfProgram` will receive a value of type `[FilePath]`[0].
This is starting to feel like a waste of time. You are very much hung up on trying to defend the idea that using `Maybe` is something to be avoided. I understand where you are coming from. I really do. But you are simply not going to convince me because I prefer to model systems as a whole and I prefer to avoid doing extra gymnastics to solve already-solved problems. Throwing an exception? C'mon... we both know that example sucks.
My critique of the post really has nothing to do with choosing `Maybe` vs validating. My critique is that the author's code is utterly failing to exemplify parsing over validation! Using `Maybe` to chain parsers together in order to build an input would have been perfect. Unfortunately, they kind of mucked it up halfway through because they appear to be afraid of `Maybe`. It's a shame given that the post seems to have gotten around.
[0] This whole `NonEmpty` non-sense is a sideshow that's not worth discussing (other than to further illustrate how `Maybe` can be used to simplify multi-step parsing). What happens when you need the Nth element? You just keep re-defining the type to include more values? When we get to `NonEmpty6` I think maybe we will have realized we are on the wrong path. For our purposes it's better to think of `[FilePath]` as `Input` and not get bogged down in the specifics of its shape. The important bit is that it might not exist.
The entire point of Maybe is to imbue some type 'a' with an extra value - Nothing - along with a tag about which case you have. So (Maybe a) is always inhabited by more values than 'a', and that is the sense in which a variable of type a is 'more precise' than one of (Maybe a). I'm not saying Maybe is bad in any way - as you point out sometimes you do have to deal with the possibility of not having a value e.g. looking up a key in a map, looking up a user from a database etc. In Haskell there's no 'null' value which inhabits each type so you have to use Maybe, but even in languages like C# or Java where reference types all contain null I would still prefer to use Maybe/Optional to be explicit about the possibility. I don't think we disagree here. But at any point in a program you would always prefer to receive an 'a' over a (Maybe a) if you had the choice since there are fewer cases to deal with. This is the same reason languages like C# are adding support for non-nullable reference types.
Type-driven design is based around encoding invariants as much as is practical in the type system (what constitutes 'practical' is constrained by the type system you're using). The (NonEmpty a) type is just used to demonstrate a very simple example of this principle. In the same way that type 'a' is smaller than the type (Maybe a), so (NonEmpty a) is smaller than a [a] which means the operations on it are similarly more precise, which shows up in the two version of head:
head :: NonEmpty a -> a
head :: [a] -> Maybe a
But this is just one example - you could replace it with different representations of a user in a web service
type User {name :: String}
type User = JsonValue
and the consequent difference in the types of the accessor for the name:
Far from being a 'sideshow' this is the main point of the approach - using a more precise representation makes all the operations on it similarly more precise globally throughout the program.
In your post the argument to restOfProgram has type [FilePath] but in the post it is (NonEmpty FilePath) so you need to handle the potential non-emptiness of the list everywhere you try to access it, either by propagating missing values to a higher level or using 'unsafe' functions like fromJust. It's defensible to prefer using a simpler representation type and dealing with the imprecision, but it's not doing the same thing - the types for a lot of the internals of your program will be quite different. This is probably the main philosophical difference with Clojure which prefers to use a small number of simple types along with dynamically checking desired properties at the point of use, something which tools like spec and schema make quite convenient. But people use static languages because of the global property checking, so it seems odd to me to endorse explicit modelling of missing values with Maybe while rejecting doing the same thing for non-emptiness since they are both lightweight approaches.
The insight of the original post is that if you choose to try make your types precise in this way (and most Haskell programers would I believe) then the process of checking the properties you want to enforce from a less-precise representation is inseperable from the process of converting into the narrower representation.
This narrowing process could fail and must therefore encode the representation for the failure case. Your insistence that Maybe should be used as the one true failure representation is wrong I think, throwing exceptions in Haskell is rare but but they could have also chosen (Either String) for example. Maybe isn't even a particularly good representation since it doesn't contain any way of describing the reason for the failure, just that it happened. I agree it would have been nice to see an example of parser composition using <=< etc. would have been useful there but it's not the main point of the article.
Lexi absolutely understands how to properly use the Maybe monad. What you're saying to do here is the exact opposite of what this post is advocating for. You're talking about pushing the handling of the Maybe till later and the post is all about the advantages of handling it upfront and not having to worry about it anymore. You might want to read it one more time.
I understand. But what is purpose of `Maybe`? The reason one would reach to the above construct is precisely to offload (pushing to later) the handling of a value that may (or may not) be present at runtime such that a developer can write code assuming the value is always present and ignore the `Nothing` case.
Sure you can unwrap it right away, but that isn't necessary because you could also just "bind" the next function call to the monad (which is more idiomatic to the construct). You never have to worry about that value in this case because... well... that's the benefit of using `Maybe`.
I'm not super familiar with Haskell, but my sense is that the author is trying more to please the compiler (at a specific point in the program!) than simplify the logic. That is, they want a concrete value (`configDirs`) to exist in the body of `main` more than they want the cleanest representation of the problem in code.
In this case, it's to provide a better error message in case there's an empty list than `fromList` would provide.
> You never have to worry about that value in this case because... well... that's the benefit of using `Maybe`.
But you do, your entire program doesn't live in `Maybe` so at some point you have to check whether it's `Just a` or `Nothing`. Once again, the whole point of the post is to argue that getting out of the `Maybe` as close to parsing time as possible is preferable so you have a more specific type to work with after that. You also see right away what didn't parse instead of just knowing that something didn't parse, which is what would happen if you stayed in the `Maybe` monad for all your parsing.
Well... if your entire program is dependent on some input that may or may not exist at runtime... then it kind of does live in `Maybe`.
I have no issue with unwrapping a `Maybe` to throw an exception. But I do find it a bit ironic that the post is about parsing instead of validating, that the perfect construct is right there to exemplify how it could be done, but the author then chooses to eschew it and instead show examples of how validation could look.
The body of `main`, for example, could be refactored to something like:
maybeInitialized <- (getConfigurationDirectories >>= head >> initializeCache)
Which actually shows how `Maybe` can be used to simplify the system. If you want to unwrap the maybe at this point to throw, go for it! But the above is a much cleaner representation of the program than what author is trying to do (it's crystal clear how the cache might get initialized). I would expect "Parse don't validate" to be about how useful `Maybe` is to combine parsing logic into a functional flow vs. how validation leads to an ugly procedural approach.
That is a valid approach in any language. Static or not. Doesn't change my point that heavily. And it is all too possible to pick a bad parsing/binding language such that protocol changes in the request are now foot guns.
I do like static typing. But honestly, no other PL¹ (statically typed or otherwise) even comes close in terms of the ergonomics and joy of writing software. Nothing is quite enjoyable for me like Clojure. Haskell is great but hard, and I'm years away from claiming I achieved production-ready proficiency with it. I don't want to berate other languages, but I looked into OCaml, F#, Kotlin, Scala, Rust, and a few others. And none of them feel to me as enjoyable as Clojure.
After so many years of programming, I finally, truly feel like I love my job. Also, I never liked Python. Maybe just a little, in the beginning. Once I get to know it, I disliked it forever.
-------
¹ I mean languages used in the industry, not counting even more "esoteric" PLs
Java doesn’t really have a nice interface for interacting with objects in general. Closure does have a nice interface for interacting with dictionaries. They have namespaces keyword symbols for keys which are much more ergonomic than typing strings, and they have lots of functions for modifying dictionaries. I think the big difference is in the philosophy of what the language thinks data is, and how the world ought to be modelled.
Walmart Labs was a step in this direction.. but we need some big companies to standardize around Clojure to jumpstart the ecosystem of knowledge, libraries, talent, etc. I’ve spoken to engineering hiring managers at fairly big companies and they’re not willing to shift to a niche language based only on technical merits but without a strong ecosystem.
If we don’t get some big companies to take on this roll the language is going nowhere.
I’m saying this because I’m a huge fan of Clojure (as a syntax and language, not crazy about the runtime characteristics) and I hope I get the opportunity to use it.
What does Google Trends have to do with a programming language? PHP is trending, and Clojure is not, perhaps because Clojure gives you a lot fewer reasons to google stuff up?
I myself rarely use Google to find a solution to a problem, and certainly almost never have to google shit like: "how to open a file in Clojure"...
I started working professionally with Clojure earlier this year and this article rings true. I think the article leaves out a fourth downside to running on the JVM: cryptic stack traces. Clojure will often throw Java errors when you do something wrong in Clojure. It's a bit of a pain to reason about what part of your Clojure code this Java error relates to, especially when just starting out.
To be fair, this is not unique to Clojure. You need to deal with stack traces no matter what as long as you're using any programming language that targets the JVM (even statically type-checked languages like Scala). There are some great articles [1][2] that discuss various simple techniques helpful for debugging and dealing with stack traces.
I've never really had a problem with stack traces in Scala. Every once in a while you hit a cryptic one that's buried in Java library code, but for the most part they're runtime errors that are due to incompletely tested code or some kind of handled error with a very specific message.
I work at Ladder [0], and almost everything is done in Clojure/ClojureScript here. I had no previous experience in Clojure – Ladder ramps you if you haven't used it before. My interview was in Python. We're currently hiring senior engineers, no Clojure experience necessary [1].
This is great to hear that Clojure experience is not a requirement! Thank you for sharing. I am based in NY and not willing to relocate, so I will look into NY/remote companies :)
Looks like the remote situation is only temporary.
From their website [0]:
"On returning to our office in Palo Alto, California
At the moment, our employees are currently living and working all over the country. When it’s safe to gather again, we fully intend to return to the office."
I have had the pleasure of contributing to their code since we used their product at a previous company I worked at, and I must say I am sold on Clojure. Definitely a great language to have in your toolbox.
> We tried VisualVM but since Clojure memory consists mostly of primitives (Strings, Integers etc) it was very hard to understand which data of the application is being accumulated and why.
I was going to suggest this -- inside of VisualVM, you can right-click a process and then press "Start JFR"
Then wait a bit, right click it again, and select "Dump JFR"
What you get is a Flight Record dump that contains profiling information you can view that's more comprehensive than any language I've ever seen.
I used this for the first time the other day and felt like my life has been changed.
Specifically, if you want to see where the application is spending it's time and in what callstacks, you can use the CPU profiling and expand the threads -- they contain callstacks with timing
There's some screenshots in an issue I filed here showing this if anyone if curious what it looks like:
I tried to use Clojure but what put me of was that simple mistakes like missing argument or wrongly closed bracket didn't alert me until I tried running the program and then gave me just some java stack spat out by jvm running Clojure compiler on my program.
> didn't alert me until I tried running the program
That's because that's not how Clojure developers normally work. You don't do changes and then "run the program". You start your REPL and send expressions from your editor to the REPL after you've made a change you're not sure about. So you'd discover the missing argument when you call the function, directly after writing it.
Interesting. How exactly that looks? Do you have files opened in your editor, change them then go into previously opened repl, and just call the functions and the new version of those function runs?
That's right. You typically would have your text editor/ide open, and the process you're developing would expose a repl port which your editor can connect to. As you edit the source code, that will automatically update the code running in the process you're debugging. See this demo of developing a ClojureScript React Native mobile app published yesterday: https://youtu.be/3HxVMGaiZbc?t=1724
Thanks to the dynamic nature of Clojure programs, experienced Clojure developers use the REPL-driven development workflow as demonstrated in this video [1].
From what I understand, instead of writing the file and running the file you write separate statements in the file and evaluate each of them in the repl (like with "Do it" in Smalltalk).
So what you get, after running the file afterwards from clean state might be different than the result of your selective separate manual evaluations.
This looks like exactly the opposite of the F5 workflow in the browser where you can run your program from clean state with single keypress.
I haven't watched the video till the end though maybe there's a single key that restarts the repl and runs the files from clean state here too.
At first glance you could have the same workflow with JS, but there's not much need for it because JS VMs restart very quickly and also you'd need to code in JS in very particular style, avoiding passing function and class "pointers" around and avoid keeping them in variables. I guess clojure just doesn't do that very often and just refers to functions through their global identifiers, and if that's not enough, even through symbols (like passing the #'app in this video instead of just app).
Almost. I don't actually ever navigate to the REPL process itself. I have my source files open in my editor, and when I want to evaluate something, I select the expression to evaluate and press my shortcut to evaluate it. Then my editor shows the results of the evaluation either inline or in a separate window next to the call.
So when I later call the function I created for example, it'll use the new evaluated code instead of the old. If I'm happy, I save the file, everything reloads from there while keeping the same state.
Being able to keep track of what data was where is the initial bump I had as well when learning Clojure. Unlike the author, I personally got used to it, and generally don't struggle with it anymore, but part of that is learning good habits on your code base where you make judicious use of names, doc-string, destructuring and have a well defined data model using records or Spec or Schema, etc.
The other one is just getting good at the REPL and inspecting the implementation for functions to quickly see what keys and all they make use of.
Something the article didn't really cover either is that it's not really the lack of static type checking that's the real culprit, its the data-oriented style of programming that is. If you modeled your data with generic data-structures even in Haskell, Java, C# or any other statically typed language, you'd have the same issue.
If Clojure used abstract data-types (ADTs) like is often the case in statically typed languages, things would already be simpler.
(defrecord Name [first middle last])
(defn greet
[{:keys [first middle last]
:as name}]
(assert (instance? Name name))
(println "Hello" first middle last))
(greet (->Name "John" "Bobby" "Doe"))
This is how other languages work, all "entities" are created as ADTs, it has pros/cons off course, which is why Clojure tend to favour the data-oriented approach where you'd just do:
Great article, love Clojure, unfortunately couldn't find any work with it when I tried, I managed to flop in the only interview I got :(
Still, I miss it sometimes when I'm writing C#.
Great article, love Clojure. Was trying to figure out what Nanit does. Might want to consider putting a link to the Nanit homepage on your engineering page. When just typed in nanit.com and saw the baby monitor tech, I thought maybe I went to the wrong place, until I saw the logos matched. Anyway, good read, but please put a link to your home page on your engineering site, or, put a 1 liner in the opening of your blog giving context to what your company does.
> Pure functions make code design easier: In fact, there’s very little design to be done when your codebase consists mostly of pure functions.
Ummm... I am a little bit fearful about your codebase.
If you don't see the need for designing your FP system it probably mostly means it is being designed ad hoc rather than explicitly.
If you are trying to compare to OOP system done right, you will notice that this includes a lot of work in identifying domain model of your problem, discovering names for various things your application operates on, and so on. Just because you elect to not do all of this doesn't mean the problem vanishes, it most likely is just shifted to some form of technical debt.
> Clojure is a dynamic language which has its advantages but not once I stumbled upon a function that received a dictionary argument and I found myself spending a lot of time to find out what keys it holds.
Dynamic typing is a tradeoff which you have to be very keenly aware of if you want to design a non-trivial system in a dynamically typed language.
It is not a problem with Clojure, it is just a property of all dynamically-typed languages.
> If you don't see the need for designing your FP system it probably mostly means it is being designed ad hoc rather than explicitly.
Why does FP seem to imply that things are designed ad hoc rather than with purpose? I've been working exclusively with FP codebases for the last 5 year, and all designs have been by identifying the domain model and implement it with purpose, with a plan.
> includes a lot of work in identifying domain model of your problem
FP does not exclude creating a domain model of your problem, discovering names and so on, not sure why you think so? Love to hear the reasoning behind this view you have.
I think you misred my comment and think I think exactly the opposite from what I wrote.
To reiterate my point: whether OOP or FP you still need to invest time researching, understanding and writing down domain model and designing your application.
And if you don't, the problem doesn't go away and instead hides in some form of technical debt.
> ... and the question regarding choosing Clojure as our main programming language rose over and over again
If I find myself having to repeat myself justifying a certain decision time and time again, it's an indicator that the decision needs to be revised to be something which is a more intuitive fit for the organization.
Not really; it's like stoplights. You're going to be interrupted and therefore notice the red lights, and just sail easily through and thus not notice the green lights. Likewise, you're going to notice the pain points, but need to take a minute to reflect to notice the benefits.
Really, if repeating the same justifications convinces people, then the problem isn't the justifications.
I donno why you're being downvoted, it's a questionable decision and probably the company would have been better off with Python/PHP/Node. Hiring and onboarding are extremely important for a startup. You know what else? Finding answers to common questions on Google/Stackoverflow; I am now working with Ember and can tell you guys you take a 50% productivity hit by using a tool that's obscure on Google. Sure once you become super familiar with a tool that matters less, but that takes time. Much more time. React/Angular may be an inferior tool to Ember but the fact that you can get answers to almost any question is priceless. The community size is super important. The frameworks are super important (is there a Closure equivalent to Rails/Django/Laravel in community size, in battle testedness? I really doubt it).
That being said, I salute these brave companies for sticking to these obscure languages. Do we want to live in a world where there's only 3 languages to do everything? Even 10 sounds boring. Hell, even a fantastic tool like Ruby is considered Niche in certain parts of the world. I don't want a world without Ruby so I don't want a world without Closure.
> Hiring and onboarding are extremely important for a startup.
If you're a small company, you usually cannot afford to hire "mediocre" talent. It is much more expensive to undo the crapola they'd implement. Trying to hire those who are at least interested in learning and using languages like Clojure, Rust, Haskell, Elixir, Elm, etc., is a very good quality filter. ROI from hiring a smaller number of Clojure devs, rather than a few more "regular" engineers - is much higher.
> Finding answers to common questions on Google/Stackoverflow;
Clojure gives you far fewer reasons for Googling things than other language ecosystems. It is dense language and inspires you to write smaller functions, decreasing the surface area for the problem. Most of the time, asking questions in Clojurians Slack sends you halfway through the solution.
> I salute these brave companies for sticking to these obscure languages
They do not choose Clojure for the shtick; Clojure is a highly pragmatic and immensely productive instrument. There are many "success stories" with small and medium-sized companies. A few large companies like Cisco, Apple, Walmart, et al., actively develop in Clojure.
The same can be said about the engineers. They don't choose Clojure because "they hate Java". You can check any Clojure surveys of the past. Most Clojure engineers are experienced and "tired" developers. Seasoned hackers who have seen the action. For most of them - Clojure is a deliberate choice. Many of them landed in it after trying various other alternatives.
> Trying to hire those who are at least interested in learning and using languages like Clojure, Rust, Haskell, Elixir, Elm, etc., is a very good quality filter.
That's not my experience. It doesn't say a whole lot, it just says a person is bored a bit and is confident in his ability to learn new things, you can filter for learning abilities by looking at what the person achieved; doing new stacks is just one metric. Also it's sometimes the type of people who care more about learning/trying new tech on the job than actually helping the business (for exmaple by introducing GraphQL because they read about it in a blog and it looks cool, not because they really think the business needs it).
Alternatively, you could document the thought process that lead up to the decision and you can point the unenlightened to the documentation instead of having to repeat yourself.
That's not a good indication that the decision was or was not correct. Only that it currently runs against whatever the established practice is. Sometimes "the way things have always been done" is just wrong.
This is unlikely to be the case in the choice of programming languages. Some may be a bad fit, some may have ecosystems that are unpleasant to use, but it's generally not the biggest problem an organization will have.
> To understand a program you must become both the machine and the program.
- Epigrams in Programming, Alan Perlis
Two of the big advantages of (gradually-) typed languages are communication (documentation) and robustness. These can be gained back with clojure spec and other fantastic libraries like schema and malli. What you get here goes way beyond what a strict, static type systems gets you, such as arbitrary predicate validation, freely composable schemas, automated instrumentation and property testing. You simply do not have that in a static world. These are old ideas and I think one of the most notable ones would be Eiffel with it's Design by Contract method, where you communicate pre-/post-conditions and invariants clearly. It speaks to the power of Clojure (and Lisp in general) that those are just libraries, not external tools or compiler extensions.