Which New Language Should I Learn for Web Development?
37 points by mtlynch
37 points by mtlynch
Language looks gross
I love commentary like this as a developer who cares deeply about style and aesthetics in everything from computing, to clothing, to dance, and beyond. Life is too short to be spent writing code that looks gross, and this alone is a completely valid reason to disqualify a language from consideration in an exercise like this.
“Gross” is so ambiguous. I respect subjective takes and it is more helpful when people unpack what they mean.
If the subjective claim is purely a matter of familiarity, it holds less weight for me.
if the subjective claim has to do with mapping a visual construct to a conceptual understanding, this is usually really interesting.
if the subjective claim is about better readability, that can be a strength
if the subjective claim is about better concision, that can be a strength
some subjective claims bake-in some preference towards ease of writing versus ease of reading
Many of these subjective claims stand in tension with each other until one specifies a trade-off function, so to speak. I think many of the debates lose their barbs when people make narrower claims and explain their trade-offs.
Haskell Bad […] I’d become a weird Haskell person
I enjoy the mix of serious and funny/ridiculous concerns xD
Moreover, I think that is a concern for any semi-obscure technology. For me that happened with FreeBSD.
I don’t remember when I was introduced to it. But now I couldn’t even in my wildest dreams think about deploying a webapp to anything but the BSDs..
Out of curiosity, what makes BSDs so compelling for webapp deployment?
There are many arguments:
top
in a brand new Ubuntu VM, then do it with FreeBSD. Usually FreeBSD will be running less processes out of the box, you’ll be working with a more slim OS. I see this on how snappy a FreeBSD box feels compared to an Ubuntu one with the same specs.There is a bunch of truth in this in that hardcore Haskell people are relatively difficult to place and socialise it always feels like.
Gleam checks all the must-have boxes.
To be more precise:
It’s significantly different from languages I know well
Functional with strong static typing. Plus running on BEAM with Erlang actor semantics and OTP. Plenty different!
Web apps are a first-class citizen
Also targets Javascript, and has a pretty cutting-edge (if not exactly mature) web framework in Lustre.
Makes it easy to build small, simple apps I want the opposite of Angular, which feels overly optimized for large projects
Gleam is designed to be light, simple and explicit.
Supports backend and frontend
Compiles to Javascript for frontend, and Erlang or Javascript for backend.
Compatible with SQLite as a data store
Yes: https://github.com/lpil/sqlight
Has good support for unit testing
For sure: https://hexdocs.pm/gleeunit/
Open-source
https://github.com/gleam-lang/gleam/blob/main/LICENCE
Actively-maintained
Amazing release cadence really. https://github.com/gleam-lang/gleam/blob/main/CHANGELOG.md
Surprised no one suggested OCaml yet; it’s the language that meets basically all your criteria. It’s kind of a cross between Haskell and Go. Check out the Dream web framework and my HTML library: https://github.com/yawaramin/dream-html/tree/todoapp/app
And this is just the server-driven side of the OCaml spectrum; OCaml also compiles to JavaScript and WebAssembly so there are many ways to share code and deploy.
I think if you’re trying to avoid magic, you probably want to stay clear of Rails. That said, I’m a huge fan of Ruby and think it is significantly different in many minor (but meaningful) ways from Python and you’d still likely learn a decent amount—particularly in the area of aesthetics. Perhaps consider using Sinatra or Hanami?
My favorite Ruby web framework is Camping because it’s sufficiently tiny that you can read and understand it in an afternoon. It gave me a big start on understanding the basic concepts that Rails uses to accomplish a lot of seemingly magical things.
I think it would be as annoying as trying to learn Portuguese when you already speak Spanish (or the other way round). Two different things but close enough to each other that you’ll always be mixed up.
As someone who studied Spanish in school and now has a Brazilian wife, I have a different take. It was easier to ramp up on Portuguese precisely because so many of the words are similar. After a few months of listening, the pronunciation differences are night and day. Sentence construction rules are pretty much identical, so that was also easy to transfer. And it’s quite fun when you come up across words that are very different between the two languages. For Brazilian Portuguese this is often because of the African and native heritages of the population.
You could also explore Clojure and Kotlin! Regarding Yesod, yes it looks ugly and I did not liked it. Also it uses Template Haskell, which is a bit controversial and can make some errors more difficult to debug
This very much. Clojure has a bunch of well developed web frameworks and libraries and it’s both sufficiently different and interesting/fun to be a great addition to your kit.
Agreed, Clojure/Clojurescript is a sweet spot for me on my hobby projects. I’m currently learning Replicant having already worked out using React/Reagent and it’s been really nice.
The thing that made me most curious about Haskell is Alexis King’s “Parse, don’t validate”, which made me appreciate static typing when I’d never cared that much before. I apply Alexis’ ideas in Go,
Not super on topic, but I’d be interested to know more about the author’s experience (or somebody else’s) with this: I think both the language and the ecosystem are leaning hard in the opposite direction.
Not super on topic, but I’d be interested to know more about the author’s experience (or somebody else’s) with this: I think both the language and the ecosystem are leaning hard in the opposite direction.
Sure, I’d like to write a dedicated post about this, but basically the way I apply this in Go is by creating a custom struct and only allowing it to create the struct through a single function that enforces constraints.
So, suppose I was implementing Lobsters in Go, and I wanted to make sure that anywhere I had a handle/username like mtlynch
, I was certain that it didn’t contain illegal characters. So, suppose the rules are that Lobsters handles can only contain alphanumeric characters and must be between 4 and 15 characters long.
My Go code would look like this:
package lobsters
var handlePattern = regexp.MustCompile("^[A-Za-z0-9_]{4,15}$")
type Handle struct {
handle string
}
func (h Handle) String() string { return h.handle }
func NewHandle(raw string) (Handle, error) {
if !HandlePattern.MatchString(t) {
return Handle{}, errors.New("invalid handle")
}
return Handle{raw}, nil
}
That way, anywhere I have a Handle
type, I know it conforms to my rules because otherwise a client couldn’t have created it.
It’s not perfect because clients can still create an empty Handle
, but I still find it valuable.
Hey, thank you so much! If I may ask another question, how do you handle using existing parsing libraries that were not designed with validation in mind (for example encoding/json)?
When I started my current job in Go I tried to introduce my team to this pattern but realized I was fighting an uphill battle against the libs already in use and gave up.
I do my own parsing on top of the JSON parsing, so to keep going with the example, imagine that the server received comments from Lobsters users as a simple JSON dict like {"username": "mtlynch", "body": "hello, world!"}
I’d have an internal type to represent a validated request that’s the result of converting the JSON-encoded HTTP request body to a validated type:
package lobsters
type CommentRequest struct {
Handle handle
CommentBody body
}
func (s Server) commentFromRequest(r *http.Request) (lobsters.CommentRequest, error) {
var payload struct {
Handle string `json:"handle"`
Body string `json:"body"`
}
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
log.Printf("request had invalid JSON: %v", err)
return lobsters.CommentRequest{}, err
}
handle, err := NewHandle(payload.Handle)
if err != nil {
return lobsters.CommentRequest{}, err
}
body, err := NewCommentBody(payload.Body)
if err != nil {
return lobsters.CommentRequest{}, err
}
return lobsters.CommentRequest{
Handle: handle,
Body: body,
}, nil
}
Thanks again! Out of curiosity, do you write this by hand, or use some codegen / reflection? Seems like a lot to do manually.
I write it manually.
It’s a pattern I use a lot, which you can see in my open-source projects. It’s a little verbose, but I like that it forces me to decide how to handle bad input explicitly for every field. But it’s also why I’m interested in a new language to see a different approach.
I’m working on bringing this advice to the JS parser ecosystem with actual immutable syntax trees. I always refer people to Lexi’s blog post if I want them to understand why
The new thing is Phoenix LiveView
LiveView is great, and makes it a lot easier to write web apps compared to SPA or traditional MVC models.
But LiveView generates SPAs
Nope. LiveView is an alternative to SPAs. Instead of having most of your logic (and data retrieval) implemented or orchestrated from the frontend, almost everything is running on the backend. Most of the time, you can forget about the client-server nature of what you’re doing (but not all of the time of course). The code turns out very simple and natural for what it’s implementing, and testing is a dream. You should try it!
Phoenix depending on Tailwind CSS by default
Phoenix is fully modular. You can simply choose to not include Tailwind (or LiveView, or most everything) when you generate your app. Also, check out https://phoenixigniter.com for many more options!
Yeah, PHP looks a little gross. It started out as a Perl wannabe and went through a Java fan phase later, and it still shows.
I like Symfony (inspired by Spring, coincidentally) better than Laravel, it feels more robust and extendable and doesn’t try to upsell you every step of the way, just a little bit.
Scheme is pretty great IMO (duh, I am a core dev). Especially sxml is a great fit for writing xml/html. At least with CHICKEN scheme there are plenty of bindings available like you want for sqlite.
Michael, you say that Elixir has “no static typing”, but iiuc, they’ve been adding gradual typing to the language for the last year. Does that meet your needs, or nah?
Yeah, I saw that in my search, but I didn’t know quite what that meant. Does that mean I can use types today? Or like in 2-3 years, I’ll be able to use types? And I’m assuming that even if I have types, it will take a while before popular packages have types.
Types aren’t a hard requirement, but I enjoy them, so that’s part of what makes Gleam more appealing to me than Elixir, as Gleam was designed with types from the ground up.
Elixir has typespecs which currently works as a documentation and can be used by some tooling (like Dialyzer) to check for some kinds of errors (though it isn’t perfect, errors are sometimes hard to read, and it works slightly different than what people may expect). Basics of set-theoretic types is there, but currently it isn’t fully there (for example you cannot type your own functions). So it is slowly getting rolled out.
The people I know who enjoy Haskell tend to be annoyingly smart language nerds who love thinking about compiler and language design. That’s not me.
As a (hopefully not too) Weird Haskell Person who is a bit of a language nerd, and does enjoy tinkering with runtimes, I’m sorry that’s the only side of Haskell that you’ve seen, because it’s also my favourite get-stuff-done language and my favourite imperative programming language. Yes, it’s fun to go down rabbit holes and contemplate infinite data structures or novel algebraic gadgets, but it’s also fun to just solve people’s problems with good tools.
Someone looking to broaden his horizons with a new language feels like the sort of person who shouldn’t bounce off Haskell. Do you have concrete suggestions about how to make that less of a thing? Haskellers proactively sharing more projects, not just blogposts, feels like a good start.
I think there are many useful mind-expanding ideas in Haskell that you’ll probably value, and I hope you give it another look at some point. It is very different and the learning curve can be a bit steep at times, but do reach out somewhere if you get stuck - there are lots of helpful enthusiasts.
Oh, I didn’t mean it to be a serious criticism of the Haskell community. My impression of Haskell developers is just me incidentally encountering blog posts or comments on forums. I admittedly haven’t given Haskell a fair chance.
I don’t think the community at large is doing anything wrong or uninviting, but I think the culture I perceive doesn’t feel like it matches my personality. And not in a bad way, just in the way that some people like vanilla ice cream and some people like chocolate ice cream.
Don’t worry, I didn’t take it as such. But I think it points at something that is real some people, and the “on-ramp” has been a recent topic of discussion so it’s been on my mind.
If and when I learn a new language, my main goal is to have the language break my brain a bit. I.e. it should make me approach a problem in profoundly different ways than what I’m used to. Not only will it make me a better programmer because I will have more ways to think about real-world problems, I believe that the cogsci research around building new neural pathways as a way to promote brain health will prove very true long-term. In that regard, I will probably make my way through the programming ur languages first: https://news.ycombinator.com/item?id=35816454
Earnest question:
I wish there were better packaging / testing tools that didn’t depend on node.js.
Can you elaborate on this? I often see people extolling “tools for a language built in that language” as a virtue - what do you dislike about it?
Sure, to compare it to something like Go, if I want a third-party package, I do go get example.com/vendor/package
and then I can just import that package in any file and use it without thinking much. Python is usually close to that where I can pip install examplepkg
and then import examplepkg
in any file (though it’s not as always that simple).
With Node.js, I never have that experience of just npm install and then I can use it. Because there’s usually some other step where I have to configure it with webpack, or if I’m using a framework like Vue, I have to find a special version of the library for that framework and then that library has its own special way that I have to import it.
I also find that API churn within the Node.js ecosystem is worse than any other ecosystem I’ve worked with. If I have to upgrade a package in a project I worked on 2+ years ago, it’s always a huge headache because the new version of package X no longer plays nicely with another package Y, so I now have to figure out how to update Y, and this goes on until I basically have to update everything, and then I find that one of the packages is EOL, and I have to replace it with some other package with different semantics.
I don’t know that this is the fault of Node.js, but just my experience in that ecosystem makes me want to avoid taking more dependencies on it.
I don’t have a requirement that tools for the language be built in that language. I haven’t explored Bun much, but I’m not sure it addresses my pain points with Node. I only use Node.js for package management, but I never deploy a Node.js server. All of my web apps are static files or templated with Go/Python, but I’ve never deployed a Node.js server to production.
Note that it is deno, not bun, that seeks to make JS package management both better and different to what it is in NodeJS right now.
Bun seems to aim for npm compatibility but with faster, friendlier and more reliable tools (noble aims to be sure).
Not OP (who already answered), but I work in dev tooling (Cloud Native Buildpacks) and bootstrapping is a crappy problem. I can run “bundle platform —ruby” to output a project’s Ruby version but … to run that I’ve gotta have Ruby and bundler already. And they’ve got to be compatible with the Gemfile.lock format and with each other. Also the bundler codebase has dependencies but it can’t use bundler to handle them because yo dawg.
I saw this thread when you two were first talking about this but it didn’t really click for me until just now. I’ve got two kind of interrelated thoughts that you might find interesting. For context, I spend most of my $WORK time these days writing production embedded C++ code and doing a ton of data analysis using Python/Jupyter.
First thought: I have no idea what language most of the tools I use are written in. We use CMake for our builds; I’d guess it’s written in either C or C++ but I’m not positive on that. We use clang-format for having consistent formatting, CodeChecker as a static analyzer, GTest as a test runner. We generally depend on system libraries and don’t worry about building from source either. On the Python side I know a lot of the libraries are going to be written in Python or C or Rust, but I don’t really know or care which any given library is written in so long as import foo
works.
Second thought: Unless you’re doing server-side JavaScript as well, the node.js dependency is a little different than most other languages. As an example, I fully expect to use an Elixir runtime to run my Elixir code. Similarly for Python code. I don’t generally care so much what language the tooling is written in, but I expect to need that runtime to actually run the code. But with frontend JavaScript… the browser is the runtime. For backend JS, sure, node.js is probably the runtime that’ll actually execute the code. Needing to have a node.js runtime when working on frontend code that won’t actually use that runtime is a bit weird. Having e.g. a JS bundler written in node.js is fine, but I’m just as happy having one written in Rust or C++ or Python or Ruby or whatever (maybe happier, for the same reasons that mtlynch points out below).
I don’t know if I’d say “web apps are a first class citizen” but I think Nim also fits most of the requirements and nice-to-haves.
But there are definitely libraries that enable productive web development. And you can compile directly to js for the frontend!
If it weren’t for a different style of language, C# would be a top contender. With multiple different approaches to web (that can be mixed) depending on your needs. I mix web API, MVC, Interactive SSR &CSR in a single solution.
ASP.NET is probably the nicest web framework I’ve ever used. Really like how they handle persistence as well.
I find it hard to find a reason not to separate the front-end from the back-end; at which point your only choice for the front-end is JavaScript/TypeScript and for the back-end it is no longer about web development as you only need to expose an API.
If you want to get a small web app in the shortest amount of time with very little back-end functionality, then nextjs can do the job and you can mingle middlewares and functions here and there.
This brings me back to this section
Gleam hasn’t reached critical mass yet, so there’s a risk that the language won’t be around in five years, but I guess that’s okay.
I can see how a language (ie: ruby-on-rails) can still survive by previous traction. I just can’t see how any new language can remain more than a toy for hobbyists. I remember learning coffeescript over a decade ago and having fun doing it. It is dead now because the stack is so complex to maintain such a project. You need a whole organization to do that and these people need to be paid. So unless you have a critical mass of users or a rich backer, it becomes unlikely.
It doesn’t help that new comers are mostly interested to follow whatever big corp has dumped on them (vercel) and call it a day.
I find it hard to find a reason not to separate the front-end from the back-end; at which point your only choice for the front-end is JavaScript/TypeScript and for the back-end it is no longer about web development as you only need to expose an API.
I agree with this in principle, but I’ve found it hard to do this in practice. I end up with two different build systems that aren’t aware of each other, so when I want to do development with hot reloading, it’s a pain.
I’ve developed a few apps where it’s Go on the backend and Vue on the frontend, but I end up with this ugly build system where Vue has to compile the app and dump it in the static folder for Go. But running live dev servers for both Go and Vue ends up being a pain. If I have to do any server-side rendering (e.g., for opengraph tags), it’s a pain because Go and Vue use the same mustache syntax (I know I can configure a different character, but it’s still a headache), and I end up with Go templating syntax in my base Vue HTML that Vue doesn’t understand.
For the last year or two, I’ve just done Go on the backend and Go templates + htmx + vanilla JS on the frontend, and it’s an order of magnitude simpler. It’s so rare for me to run into the headaches I had with different stacks for backend and frontend.
If you would ask this in 2 years, I would suggest to try Ryelang. It does check most of your requirements … now back to the future :)
Web apps are a first-class citizen
Supports backend and frontend
Easy TypeScript/React/RSC recommendation. Hard to recommend a framework though.
Makes it easy to build small, simple apps
This takes Next.js out of the equation, which is the only “stable” solution as of now. Redwood SDK seems promising but (mostly) ties you to a specific provider, and not yet stable. Hope you have a few years to wait!
Note: liveview are not SPA, quite the contrary.
They are here to handle the few non static part of a static site. There are still page reloads and all
JS, because once you know it you can understand what code the web browser thinks you wrote. Knowing JS, you will likely be able to learn teanspile-to-JS languages like Elixir much more quickly I suspect
Web development is about writing HTML. You can do that with XQuery. It’s a functional language that works like XSLT but with an elegant syntax. I recommend BaseX to run it. I mostly use it for static websites.
In the past, I’ve had good experiences with Yesod. The type checking really goes far in a good way. The URLs for endpoints are typed. This helps to avoid dead links from typos in the template language.
You might like Rust? Web apps maybe aren’t first class but there are plenty of libraries and a few frameworks (equivalent to micro frameworks, not like Rails). There are good HTML templating libraries. SQLite bindings are very good.
I know it may seem overkill or too complex for web apps but my experience is it can be surprisingly productive. A lot of web app code is marshalling between different representations (i.e. database row > domain object > json resource) which Rust is quite good at thanks to Serde, From / Into traits, and iterators. You can “parse, don’t validate” everything with very little effort (look at Axums extractors).
If you have to step off the happy path, Iike writing your own Tokio streams or Tower services for example, it can get really complex dealing with the types. But overall I think it’s a net win using Rust. The real world sqlx axum project is a good demonstration of a Rust web app.