Do you think the lack of static typing makes using ClojureScript in the frontend hard to maintain, when the scale of a project becomes larger?

56 Comments

Share

Save

Hide

Report

90% Upvoted

This thread is archived

New comments cannot be posted and votes cannot be cast

Sort By: Best

User avatar

level 1

didibus

·1y·edited 1y

Before we go on talking about static types. I want to point out that the rate at which the "scale" grows in ClojureScript is much lower than statically typed languages that I've used.

So a 10k loc ClojureScript code base could have already been a 30k loc code base in a static language.

This means you get more leeway, so even if it did make the code harder to maintain at larger scales (and I don't believe it does as I'll talk about later), when you factor in how the equivalent program will be of a smaller scale in ClojureScript, it could still come out as a winner in terms of maintainability, because it'll have a smaller surface area for equal functionality.

With that important aspect of consideration out of the way. I'll say that the lack of static types doesn't inherently makes it less maintainable, but if you find yourself having created a monster, the lack of static types might make it even harder to maintain.

What do I mean by monster? I mean a really messy code base. I mean circular code dependencies multiple depth apparts. I mean call stacks that go 10, 100, heck thousands calls deep. I mean the same data-structures shared with every piece of logic, even the logic that doesn't even need it. I mean variables names that change their meaning depending where you are, or don't mean anything at all. I mean functions so long you need to scroll the page down. I mean shared mutable state shared by every single module of your app. I mean layers and layers of abstractions hiding away the database so when you need to trace back where something actually came from it'll take you all day to unravel the chain the data went through.

All this translates into: a code base that's like a house of cards. Change any one thing anywhere, and every other thing everywhere breaks. To add any feature, all functions need to be modified together.

ClojureScript doesn't magically prevent you from creating such a monster. And if you do, you will suffer, and you won't even have static types to protect you.

Static types don't magically prevent you from creating such a monster either. In fact, in my experience, it makes it easier to do so. Though that's unclear to me if it's the static types, or just that non Clojure languages that I've used, due to their other differences are more prone to it.

But I feel static types gives you a false sense of control, which means you don't think as hard about the monster you're creating, and you start to get lazy and rely on the compiler, and that seem to somewhat influence people to be more complacent and somehow only realize they created a monster once there's no turning back anymore.

There's also some patterns in how to structure your code which static types forces you into (at least the statically typed languages I've used), which I find automatically makes things more complicated and more difficult. Like it adds some unnecessary layers and ceremony especially between the data and me, which seems to make doing what I want more difficult than it should. Again, I can't say if it's the types per say, maybe it was the OOPness, or some other things about the language versus Clojure/Script. But still feels like the types played a role, because you can't just have heterogeneous data-structures, so you've got to maneuver your data into these strict containers which can have well typed definitions, and that's just really rigid sometimes.

So my conclusion here is, as long as you put effort and focus on not creating a monster, ClojureScript can scale and remain maintanable. If you end up with a monster, you might wish you had static types, unless the equivalent static typed monster is also 2x the scale, than you might still have wished you'd used ClojureScript.

23

User avatar

level 2

teapotwarior

OP·1y

What do I mean by monster? I mean a really messy code base. I mean circular code dependencies multiple depth apparts. I mean call stacks that go 10, 100, heck thousands calls deep. I mean the same data-structures shared with every piece of logic, even the logic that doesn't even need it. I mean variables names that change their meaning depending where you are, or don't mean anything at all. I mean functions so long you need to scroll the page down. I mean shared mutable state shared by every single module of your app. I mean layers and layers of abstractions hiding away the database so when you need to trace back where something actually came from it'll take you all day to unravel the chain the data went through.

All this translates into: a code base that's like a house of cards. Change any one thing anywhere, and every other thing everywhere breaks. To add any feature, all functions need to be modified together.

So anything that looks like typical PHP, ha ha! I think this sounds like a nightmare. Like if there is one tiny thing changed that you needed to complete your job. I think it is not really a problem with Clojure but in languages if you use var instead of let, or you don't use const that shit is messed up. When some smart arse decides not comment their code properly, is painful too. lol! I definitely with the function thing. Like if the function is so big it needs a whole page it is probably common sense to split it up into smaller pieces and remove redundancies. I like your house of cards analogy I think it sums it up nicely. Spaghetti code sucks!!

There's also some patterns in how to structure your code which static types forces you into (at least the statically typed languages I've used), which I find automatically makes things more complicated and more difficult. Like it adds some unnecessary layers and ceremony especially between the data and me, which seems to make doing what I want more difficult than it should. Again, I can't say if it's the types per say, maybe it was the OOPness, or some other things about the language versus Clojure/Script. But still feels like the types played a role, because you can't just have heterogeneous data-structures, so you've got to maneuver your data into these strict containers which can have well typed definitions, and that's just really rigid sometimes.

I am not sure what language I was using, but I remember I was playing around with it and I was getting such a headache using it because of how the typing worked. It may of been a bit esoteric though, so maybe I can't really blame it.

For me I don't really like things that are too rigid anyway and prefer things that are a bit more fluid. Maybe that is why I like Clojure so much, so far.

So my conclusion here is, as long as you put effort and focus on not creating a monster, ClojureScript can scale and remain maintanable. If you end up with a monster, you might wish you had static types, unless the equivalent static typed monster is also 2x the scale, than you might still have wished you'd used ClojureScript.

This reminds me of that saying what you reap is what you sow.

I guess if you follow good practices then there will be less that comes back to bite you anyway. Even if my code is good I will probably want to come back to ClojureScript, just because what I have seen so far has impressed me a ton.

I really enjoyed reading your post. You seem like you are quite bright.

3

Continue this thread

User avatar

level 2

rcorrear

·1y

Very well put!

2

User avatar

level 2

J_M_B

·1y

I mean shared mutable state shared by every single module of your app.

This is the big problem with re-frame.. it leads to exactly this kind of code.

1

Continue this thread

User avatar

level 1

agentbellnorm

·1y

I don’t understand why people keep implying that typing will give you good architecture.

11

User avatar

level 2

didibus

·1y

Very true. People are surprised sometimes when they see me code in Clojure and I'm defining the data model, the operations, what state I need to keep track, etc. They think: "hey I thought Clojure was dynamically typed?"

That's when I realize people from a statically typed background, designing code architecture for them is the act of coming up with the types and the classes to use. So if those aren't there, it's hard for them to think how this could be done.

7

User avatar

level 2

teapotwarior

OP·1y

Do you have any recommendations for me to learn about good architecture?

5

Continue this thread

User avatar

level 2

bonega

·1y

I think they are implying that given a project with architecture x, it is easier to refactor when using static typing.

On the other hand, dynamic typing might just result in a different architecture that is inherently easier to reason about.

My own experience seems to indicate that static typing makes it easier to refactor especially when the original developer has left.

I will await a double blind meta study to form a firm opinion

2

User avatar

level 1

pavelklavik

·1y

No, static or dynamic typing has nothing to do with maintainability of software. When you work in the world of objects, you are screwed unless the compiler/editor constantly checks for you, because every object is used in completely different way. In ClojureScript, you mainly work in terms of data. You might have some main data pieces occuring everywhere in the code and those you will document/spec. When some data are used only within one namespace or a few functions, its structure is apparent from the context (and destructuring greatly helps). Also, if you used namespaced keywords, IDE allows you to search for them within the code which is great.

In our startup, we have about 30k of frontend code, and it is very well maintainable: the best project I ever worked on. It is build using Re-frame, so we can easily inspect db atom for the application state using REPL, etc.

12

User avatar

level 2

retief1

·1y

In my experience, I ran into the most problems during refactoring. I change one function over here, and that means that some other function over there also needs to change. However, I'm not actively working on the code over there, so I don't immediately notice that it broke. Unit tests are great for this, but unit testing frontend code is hard. Meanwhile, static types work fine on the frontend and provide a modicum of safety.

8

Continue this thread

User avatar

level 2

teapotwarior

OP·1y

No, static or dynamic typing has nothing to do with maintainability of software.

This idea of static typing being more maintainable seems to be shared a lot on the net, but I believe you and think it must just be a common misconception.

When some data are used only within one namespace or a few functions, its structure is apparent from the context (and destructuring greatly helps). Also, if you used namespaced keywords, IDE allows you to search for them within the code which is great.

This seems like this would make things a lot easier down the road. This just seems like a good practice to stop yourself shooting yourself in the foot later. :)

In our startup, we have about 30k of frontend code, and it is very well maintainable: the best project I ever worked on. It is build using Re-frame, so we can easily inspect db atom for the application state using REPL, etc.

If you don't mind me asking, how many people does it take to maintain it and what makes this the best project you have worked on? I was just reading about a startup that switch from something else to Re-frame, I wonder if that was you guys.

3

Continue this thread

User avatar

level 1

vine-el

·1y

I don't have experience with a large ClojureScript codebase, but at work we have about 100k lines of Javascript. JS code written in a functional, classless style is way easier to maintain than our backend Java code. Dynamic typing also gives you more tricks to grow the code in a backwards compatible way.

In my opinion, the benefit of using functional programming with something like React is at least 10x the benefit of static typing without the costs of static typing.

I've had the opportunity to work on a massive million+ LOC Java codebase and I'd argue that static typing doesn't scale. Eventually your IDE slows to a crawl, builds take longer and longer, and threading data through a long series of types takes up a large portion of your team's time. But I haven't worked on an equivalently large dynamically typed codebase, so YMMV. Maybe the only scalable option is not having a large codebase.

3

User avatar

level 2

teapotwarior

OP·1y

I don't have experience with a large ClojureScript codebase, but at work we have about 100k lines of Javascript. JS code written in a functional, classless style is way easier to maintain than our backend Java code. Dynamic typing also gives you more tricks to grow the code in a backwards compatible way.

No worries, I like anything to do with functional programming. That is a pretty hefty codebase there. Since JavaScript is an great example of a dynamically typed language, it is great to know that this does not seem to be much of a factor.

In my opinion, the benefit of using functional programming with something like React is at least 10x the benefit of static typing without the costs of static typing.

I have been looking into using React with ClosureScript and it looks really appealing to me, so if what you are saying is true this should be a no brainer for me.

I've had the opportunity to work on a massive million+ LOC Java codebase and I'd argue that static typing doesn't scale. Eventually your IDE slows to a crawl, builds take longer and longer, and threading data through a long series of types takes up a large portion of your team's time. But I haven't worked on an equivalently large dynamically typed codebase, so YMMV. Maybe the only scalable option is not having a large codebase.

I think the guy above said it didn't scale well either. This is not the first time I have heard this. I guess if it takes more time to write statically typed code and it doesn't really add much of a benefit when a project gets too large then I can't see the point of it. There are probably a lot of pros and cons to both static and dynamic typing and maybe it might not matter that much in the end.

2

User avatar

level 2

retief1

·1y

It's worth noting that static typing =/= oo or java. Java sucks, but there are plenty of statically typed languages that support functional programming very well.

1

User avatar

level 1

yogthos

·1y

My team has been using ClojureScript for around half a decade now, and we have a few fairly large front-end apps. Here's a talk about one app that we've develop. We haven't found lack of static typing to be a problem when maintaining large long living applications.

My experience is that dynamic typing is problematic in imperative/OO languages. One problem is that the data is mutable, and you pass things around by reference. Even if you knew the shape of the data originally, there's no way to tell whether it's been changed elsewhere via side effects. The other problem is that OO encourages proliferation of types in your code. Keeping track of that quickly gets out of hand.

What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don't provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.

My experience is that immutability plays a far bigger role than types in addressing this problem. Immutability as the default makes it natural to structure applications using independent components. This indirectly helps with the problem of tracking types in large applications as well. You don't need to track types across your entire application, and you're able to do local reasoning within the scope of each component. Meanwhile, you make bigger components by composing smaller ones together, and you only need to know the types at the level of composition which is the public API for the components.

REPL driven development also plays a big role in the workflow. Any code I write, I evaluate in the REPL straight from the editor. The REPL has the full application state, so I have access to things like database connections, queues, etc. I can even connect to the REPL in production. So, say I'm writing a function to get some data from the database, I'll write the code, and run it to see exactly the shape of the data that I have. Then I might write a function to transform it, and so on. At each step I know exactly what my data is and what my code is doing.

I typically care about having a formalism is at component boundaries where the component API lives. Spec provides a much better way to do that than types. The main reason being that it focuses on ensuring semantic correctness. For example, consider a sort function. The types can tell me that I passed in a collection of a particular type and I got a collection of the same type back. However, what I really want to know is that the collection contains the same elements, and that they're in order. This is difficult to express using most type systems out there, while trivial to do using Spec.

Another aspect of Clojure is that common data structures are what's used throughout the language. The vast majority of the code ends up being type agnostic as we're generally chaining higher order functions together. Things like map, filter, reduce, interpose, don't really care what collection they're iterating over. The functions that operate on concrete types are passed in as parameters. This approach pushes the code that cares about the concrete types to a shallow domain specific layer.

The common argument is that it becomes hard to maintain giant dynamically typed applications with millions of lines of code. However, I find that there is very little value to building monolithic software in the first place. It quickly becomes complex and difficult to maintain regardless of the typing discipline. At the end of the day the developer has to understand how all the pieces of a particular project interact with one another. The more coupling there is between the components the more difficult it is to reason about the overall functionality.

Each function represents a certain transformation that we wish to apply to our data. When we need to solve a problem we simply have to understand the sequence of transformations and map those to the appropriate functions. The functions capture how the tasks are accomplished, while their composition states what is being accomplished. Code that separates what is being done from how it is done is referred to as being declarative.

Exact same model should be applied at project level as well. The project should be composed of simple components, that each encapsulate how things are being done and the way we combine them states what the overall project is doing.

5

User avatar

level 1

humorless_tw

·1y

I do not have the experience of building really large frontend project. However, I think that if I build large frontend project with re-frame and spec, it will be not too hard to maintain. The primary reason is that re-frame's interceptor design clearly separates coeffects, effects from pure event handler, which makes everything simple.

3

User avatar

level 2

teapotwarior

OP·1y

I think the guy above would agree with you. What do you normally do with ClojureScript then? Working on any hobby projects? Also, I am a big fan of anything that makes my life simpler. ^^

1

Continue this thread

User avatar

level 1

slifin

·1y

I think every project has a complexity budget, and there are certain things you can do to give you more leverage, using static analysis like clj-kondo is something you can do to give you more leverage

Most type systems in the long term will reduce your budget, most devs delude themselves into thinking you should play the game of defining what a person is, in such a way that is durable and checkable throughout time, that is the mistake most type systems and SQL tables (and other tree-based systems like file systems) make

I appreciate checks against language primitives i.e. this is an int, this is a string, this is a sequence but when you start playing with aggregates and keys with requirements there's where the misappropriations happen, most dynamic things like people are multiple dimensional and cannot be well represented with a single tree, nor N trees via union types

In terms of Clojure, I'm happy with clj-kondo's checks, it'll error on things like this (inc "test") that's the value I wanted out of a primitive type system, type systems are not schemas don't get encouraged into thinking oh I know one dimension of my person I'll set that into concrete and burden my application on every compile with that mistake, then eventually set everything to nullable anyway when you discover not all people have first names or last names etc

That's when the nullable or optional "fun" begins, I fully typed it but I still have to check that my properties exist, at the end of the day this all comes down to function requirements, and what to do when you don't have everything you wanted in your function args, the other issue is you need a data schema to recognise that these properties move together, this is kind of what spec 2's schema and selection is

It's a shame we don't have built-in support for multi-arity functions based on whether associative names exist or not, (I think it's order based currently), with associative properties the order is often incidental

2

level 1

[deleted]

·1y

If you mean SPAs yes, web UIs is where I have seen that statically typed languages really shine IMO.

1

User avatar

level 2

teapotwarior

OP·1y

Why would you say that is?

3

User avatar

level 1

Altruistic_Courage_3

·1y

With clj-kondo

https://github.com/borkdude/clj-kondo/blob/master/doc/types.md

You can type hint things like my hash map to this function must contain these keys or you can use normal Clojure type hints to let the static analyser know what should be returned/given and get immediate errors in your editor

Add clj-kondo to your CI pipeline and you could start enforcing this across teams

1

User avatar

level 1

hamgeezer

·1y

No