Relaxing a bit for my one week of unemployment before starting a new job. I got sidetracked in a MTA subway arrival time server written in Java, so now I’m working on a simple build system. Basically the goal is to have a TOML config for the basics and a Build.java file for any extra build nonsense. I think this should cover most of my personal needs. Also I’m using some module magic to allow multiple isolated versions of the same library to avoid jar hell.
This the opposite of my experience, but our workflows are different. I’ll just note some points here:
And if you encounter a problem, the most likely solution is “hidden” somewhere on Reddit or in a Github issue.
I have learned to love Zig’s source code (I mean the standard library, not the compiler). It’s very easy to read (for me, at least) and I’ve found because of the explicitness of the language design, I don’t need archaic hidden knowledge to understand what the source is doing. As a result, I’ve stopped searching for answers online and have embraced the code as the source of truth. I’ve gained a better understanding of what’s going on as a result as well which is a bonus.
But my main gripe so far is the file organization. In Go, everything in a directory ends up in the same namespace. Compared to that, Zig feels like a regression: every file needs to an explicit import. This incentivies creating very large source files.
I don’t quite agree with this. The explicit namespacing for files gives you a tool to make logical separation in your directory. I’ve found Go’s implict namespacing confusing because when I’m reading other people’s code, I never seem to know where to find a symbol (I’m not considering external tools like lsp or grep which I need in Go, but don’t in Zig). In Zig, I can just see which import the symbol refers to. It makes nice and clear.
Worse yet, tests in Zig live alongside the source code, making files even harder to read.
Again, maybe it’s my workflow, but I’ve found having the tests right beside where the code lives gives me the best tool to learn what the code does, and it never goes out of date. Taking a look at e.g. http server code in stdlib, you can read the code and then how it’s used, and that’s all you need to do. No praying, grepping, hoping, aligning the stars, searching online, banging head against the wall, to maybe find how a piece of code is used. In fact, this is why I’ve started reading the code, because it’s so easy and takes so little time to read the code and see how it’s used.
I encourage you to consider this workflow. I don’t use this flow with any other language because it just doesn’t work: too many redirections, implicitness, and fluff get in the way and you need advanced tools. In Zig, it’s all there, right there, in front of you, nicely collected together. No need for tools. This workflow has been so effective thanks to Zig that I haven’t felt the need to setup lsp yet! I just don’t feel like I need it.
I do something like that, but with LSP, that takes me right to the source/tests. Works offline, no need to even start my browser.
It also leads me to discover better ways to do what I wanted to do, because I spot a function in a module somewhere. E.g. seeing I can do bufferedReader(reader) instead of BufferedReader(@TypeOf(reader)){ .unbuffered_reader = reader, ... }.
That said, different things work for different people, and I hope the docs will one day be as enjoyable as the language.
Worse yet, tests in Zig live alongside the source code, making files even harder to read.
Again, maybe it’s my workflow, but I’ve found having the tests right beside where the code lives gives me the best tool to learn what the code does, and it never goes out of date.
Having only played with Zig, I agree with this. I prefer tests alongside the code. Drifting the topic a bit, I very much like D’s support for unit tests in the same file as the code and that they can produce documentation.
+1 it’s great in Rust as well. I also like the ability to test module-private functions (I know some people think this is bad). Java is such a pain needing to have a mirrored folder structure for the tests just so things can be in the same package.
Arrived to test-right-beside-code independently when working on Next Generation Shell. I have two groups of tests. One is basic language functionality, it goes separately and is practically not used anymore. The second is for the libraries, where each test lives just below the function.
I’ve noticed that the tests below the function are almost identical to examples that are given in documentation which is just above the function. This needs to be addressed.
I find that I have this attitude a lot more now in code reviews. I’ll see something and I’m pretty sure there’s a better way to structure things, but taking the time to figure that out, explain it, potentially argue about it, and then re-review the updated code is a lot of effort on both my and their part. Especially if their code is functionally fine.
I have side projects to give a fuck about doing things the “right” way 🙂
The text format is nice, and having a schema is great.
The downside, of course, is that you’re using protobuf. Protobuf is amazing – it shows how much code you can squeeze into a relatively small amount of functionality. And it churns surprisingly quickly. Especially when you consider how little the wire format changes.
Statically-enforced types are annoying when you don’t know what types you need and you want to explore the solutions with code.
A great thing about Ruby is you can use hashes and strings to figure out what you need (same with JS objects) and then create actual types once you’ve got some code working that has allowed you to understand the problem.
With stuff like Java or Go, you are fighting the compiler instead of understanding your domain.
I have the opposite experience: Whenever I’m using Rust, SML or Haskell, I always start with defining my datatypes, and use those to understand my domain. The rest of the code flows naturally from there.
In other words: I explore solutions using datatypes.
Same. And if I don’t know what type I’ll need (like u64 or i32 or whatever), then I can use an alias or wrapper type like in the OP so I can change it for every usage in one location. Starting with types helps me build a model of the problem domain before writing any code.
This idea that types make you “fight the compiler” makes little sense to me. On the contrary, they enable the compiler to help me ensure that what I’m writing is correct from the start, free of needless debugging of runtime problems that types would have prevented.
What I’m referring to here (and should’ve made more clear) is exploring a user experience. With something like Rails, I can create a user experience very quickly, and have it using real data, real third party APIs, etc etc. This requires extremely fast iteration, often making drastic changes to see how they feel or how they work. Static typing would introduce two new steps that aren’t providing value for this particular activity: explicitly defining types, and requiring that the entire app conform to all type checks.
These two steps seem absolutely valuable in production, but for prototyping in an existing app, iterating on user experiences and design, they provide negative value and make the process harder.
Depends on what language you use. Most MLs have type inference, even Rust.
requiring that the entire app conform to all type checks.
When you’re writing something in a dynamically typed language you also need to ensure that your types match up, you just won’t know if they do until you run the code. Trivial example, 3 + x, were x is a variable containing the string “foo” will cause an exception at runtime in Ruby. I think it’s reasonable to argue that having that check happen at compile-time makes prototype development faster.
[…] create a user experience very quickly, […] often making drastic changes to see how they feel […]. […] app […]
I wonder to what extent the difference between the two sides here is a difference in perspectives between (Web or other GUI) app developers and non-app developers.
I often use that as a first step toward introducing stronger types in a code base written in a strongly typed language by developers who haven’t yet taken to actually using the stronger type system.
Sometimes after adjusting the datatypes, I want to test one function. I know I’ll have to update everything eventually, and I appreciate the compiler’s help with this, but I’d rather try running one example end-to-end first. I shouldn’t need to update code that won’t be used in that example.
I think -fdefer-type-errors is supposed to achieve this, and when I used Java and Eclipse it could do this. I could run the project without fixing all the red squiggles; it would just turn them into runtime errors.
Like the article says, it does not pay for itself. Ruby “domain code” in the wild is full of type errors, nil references and preventable bugs. I did an analysis recently and over 70% of all our exceptions that led to 500 errors were things a type system would’ve caught. This wasn’t a surprise, all projects I’ve ever been had this class of errors.
There’s little point in writing domain code quickly but faulty. We really need to disavow ourselves from this notion that static types are 100x slower to write: they’re not. You might throw something together in a day in Ruby, but it takes 2 days in Go or something else. This 1 day difference will not make your product fail, specially since you have taken the time to prevent some invalid states along the way.
This is coming from someone with 16+ years of Ruby experience, I don’t dislike the language at all. “Fast to write” is just not a good measure of quality.
There’s little point in writing domain code quickly but faulty.
Pity that domain code might be dynamically typed. If we have a DSL for our domain, the DSL compiler knows a lot, and thus is in the best position possible to enforce a crapton of invariants up front. I reckon those static checks aren’t free to implement, but they’re likely worth it. Else why are we paying the cost of a DSL to begin with?
Go, while statically typed, is arguably not strongly typed. It’s firmly in the bottom of the uncanny valley of programming languages, which can neither handle nor prevent errors. Been meaning to blog about that …
Newer versions of Go do have something close to enums. You can define something like this:
type MyEnum int64
const (
A MyEnum = iota
B
C
)
This will disallow implicit casts from other kinds of integer, which prevents other kind of integers being used by accident. The mildly annoying thing is that these are not namespaced in any way within a package so you can’t have two enumerations with the same value (just like in C, something C++ fixed long ago).
The standard library contains stuff that doesn’t make sense in terms of types, such as multiplying two Durations to get a Duration
That’s not really an artefact of the type system, that’s just the standard library authors getting their units wrong.
What about something like Typescript? You can still create arbitrary objects that lets you explore, but you still have static typing. The type inference removes most of the overhead of writing types as well.
I used to think that, but I’m leaning away from it now. I would say that whatever language you use, 75% of your code ends up having very simple types, so there’s no real gain from having dynamic typing. Dynamic typing is bad for exploratory programming with these because if you decide that the arguments should be c, b, a instead of a, b, c, you can’t use an automated tool to fix it. You do save the time of declaring the types up front, but it’s only a net gain if you happen to get it right the first time.
Where dynamic typing is useful is when you have a complicated type relationship that would be a pain to spell out. These don’t happen often, but when they do, then dynamic looks better. In a dynamic language you might say f can take an x, a y, or a z, but in static, you just say f takes an x and write a y to x and z to x converter or add fy and fz methods. And that’s a relatively simple case.
The other thing about static typing is that it sometimes buys you better performance. That doesn’t apply to TypeScript, but it is nice to have when you have it.
Dynamic typing is bad […] because if you decide that the arguments should be c, b, a instead of a, b, c, you can’t use an automated tool to fix it.
This is factually incorrect. Not only automated refactorings like this exist today. But the earliest example of automated refactorings that I know of are for a dynamically-typed language, Smalltalk.
Note that this is not directly related to the type system of the languages. But whether one uses runtime reflection vs static analysis.
When I say “can’t” I don’t mean “can’t with any possible amount of effort.” I mean “I can’t in the dynamic languages I use today with the tools available to me now.” If you have a tool that can detect signature breakages in Python and JavaScript without using static type annotations, I guess I’d like to add it to my tool chest, but for most people, the way they do it is by adding static types and using MyPy or TypeScript to catch the breakages.
When I say “can’t” I don’t mean “can’t with any possible amount of effort.” I mean “I can’t in the dynamic languages I use today with the tools available to me now.”
The discussion is about typed vs untyped, so it seems sensible to me limit the discussion to things that are inherent differences. Not things that are different between ‘popular’/‘mainstream’ typed vs untyped languages.
I don’t understand why you think automating this refactoring is so difficult in dynamically-typed languages. Or why you (implicitly) think it would be easy in statically-typed languages.
Suppose you have a function in a hypothetical statically-typed language, which has the following signature:
If you believe an IDE should be able to perform this refactoring correctly, why do you believe it would not be able to perform the same refactoring correctly in a dynamically-typed language?
The stated problem is a refactoring which changes the order of the arguments from a, b, c to c, b, a. If this is something that tooling for statically-typed languages can handle, then it must also be something that tooling for dynamically-typed languages can handle. Effectively, I’m saying that if the tooling can perform this refactoring for:
A way to illustrate my point, globals()["my_var"] = new_value. How should a refactoring algorithm detect you’re doing something like this with arbitrary strings, or what I’ve actually seen is globals()["var" + dynamic] = new_value?
I don’t understand how this is relevant to the actual hypothetical posed.
Also, I don’t understand why you think runtime modification is somehow only a thing in dynamically-typed languages, because it very much isn’t, and nobody suggests that it’s impossible to refactor, say, Java codebases when they rely on tons of runtime configuration, runtime injection, etc. making it impossible to know ahead-of-time which code paths might execute or with which values.
I finally bit the bullet and made a separate typed AST for Sasquach. I finished updating the type checker to create the TAST, now I need to plumb it through to a couple places so I can actually use it in the bytecode generation step and get rid of all of the map lookups I’m currently doing.
Ever since seeing this post about using preconfigured templates for log compression, I’ve been wanting to automatically detect log message templates. Lo and behold I just found this repo that has several algorithms to do just that, hopefully I’ll actually be able to incorporate one of them.
Not exactly a hot take, but I think virtual threads will largely spell the death of the reactive programming style in Java. Of course there are problem spaces where its conceptual model is a better fit than a thread-centric model (e.g., because backpressure is a first-class concept) but from what I’ve observed, the vast majority of people who are using reactive libraries are looking to support large numbers of concurrent clients and are tolerating the reactive model to achieve that goal. Virtual threads will be a much better fit for those people.
Of course, this won’t happen overnight, but I’m guessing with the release of Java 21, we’ll see a sharp drop in the number of new reactive projects.
Agreed. I maintain Manifold, an old streaming/CSP-style lib for Clojure, and a year ago, a fellow stream lib implementer and I were discussing the way forward with vthreads, what to do about backwards compatibility, etc.
And yeah, one of my conclusions was that if vthreads existed then, Manifold would probably not have been written, it wouldn’t have made enough sense. Vthreads cover much of it, and structured concurrency will cover most of the remainder.
Netty, and other event-driven servers have similar situations. Anything that revolves around managing a thread pool, really.
Java, the language, is built for blocking I/O. This never changed and you can see it in how its syntax requires blocking I/O. Examples:
checked exceptions;
try/finally or try-with-resources;
intrinsic locks, and the standard mutexes and semaphores.
Java with reactive stuff is basically using a Java subset, and isn’t in the language’s character.
OTOH, “reactive” is really an euphemism for function composition, which will never go out of fashion, but which always strikes fear in the hearts of developers, especially when the word “monad” gets used.
I agree with your general sentiment. This Loom feature brings Java closer to Erlang.
Java will still be missing the monitors, the nodes (so the Erlang’s OTP), but the VM were a function call can spawn a thread, and have the function executed in that new thread – will probably bring more of Erlang-style idioms into Java going forward. And that’s a good thing.
Continuing to refactor a bunch of code in the Sasquach compiler so it’s easier to maintain going forward. I replaced a bunch of uses of Strings with Java’s ConstantDesc classes and I’m gonna replace my crappy dispatch code with dynalink.
I might try to write a short post about how awesome dynalink is.
I personally would be weirded out if I received automated messages like these. If you really want to stay in touch with me, why not have a recurring calendar event? If I had an SO who used this to say “Miss you” at random times, it would feel meaningless to me. It would feel a lot more meaningful if there was a typo and I knew it was written deliberately—until the bot gets smarter and inserts random typos.
Oftentimes, when the app reminds me to reach out, I’ll write whatever I’m feeling at that moment and not use one of the templates. Other times I’ll be busy with something and one of the templates is spot on with how I’m feeling. I tried using recurring calendar entries in the past, but there wasn’t enough there to keep me consistent.
Based on the feedback here, which I agree with, it might be better to have suggestions for topics to message about or prompts that could remind you about something about that person rather than a full on prepopulated message. E.g. “Ask them about their latest travels” or “Message Bob Smith about his favorite hobby “.
This is a great writeup! I thought about going down the same path with my language, but it appears you got much further along 😀 I haven’t thought it through too much, but for the Ord case could you add an implicit Eq parameter to signify the dependency?
On another note, I also wanted to have something like derive for these modules but I’d like to avoid a whole other syntax for macros. I was thinking of something like comptime specifically for that but still have first class generics.
…for the Ord case could you add an implicit Eq parameter to signify the dependency?
I thiiiiink so, but depending on how it’s done the compiler might have a tough time proving that two different implementations of Ord(T, Eq(T)) actually lead to the same Eq value? I’ll have to think about it more, probably with more coffee in my system.
On another note, I also wanted to have something like derive for these modules but I’d like to avoid a whole other syntax for macros.
I definitely intend to have some sort of macros or comptime code generation in Garnet, but yeah the syntax is a bit of a bear. I actually find Rust’s template-y macros pretty sensible most of the time, once you climb the learning cliff of figuring out how args and repetition works, but there’s reasons I haven’t tried to tackle macros for Garnet yet. :-)
Personal: try to actually deploy my pivoted version of Eventlandr this week. The new version is centered around figuring out which friends are available rather, than planning a specific event ahead of time. I need to shore up the Vite production build of the JS code and ensure the backend serves it properly and create the Dockerfile for the Fly.io deployment.
I might blog about the idea for the site itself or how I sorta built a custom Java framework centered around server-side rendering.
At #PreviousJob, we used BuildKite, and it was fantastic - you could deploy the agent pretty much anywhere you wanted, and I found the configuration much easier than any other system I’ve used (such as GitHub Actions).
I recently realized that what I really want out of a CI system is running locally. Ideally, with decoupled backends, so, it can run tasks in containers, but also just processes, for simpler projects.
Most CI configuration languages don’t really let you do that, so you need to duplicate the commands for building, linting, testing, etc.
There’s that terra something tool, but it requires containers always, I think.
I had a couple (very) rough ideas on a CI system. One was to make the actual task configuration a Lua script with a bunch of predefined functions to make a declarative pipeline possible, but to also allow the user to drop into more imperative steps. Using Lua lets you more effectively sandbox the tasks than using something like the JVM, the runner could be much leaner, and users could possibly get real autocompletion for their steps instead of only relying on docs for some made up yaml DSL. I also really want to integrate it more deeply with metrics, so you can see annotations in Grafana when you deployed and have automatic rollback when something goes wrong.
Integrating webauthn into my social webapp to enable registration without needing an email or phone number. The once ios safari web notifications arrive, I’ll be able to have a full app without PII.
I saw this before, it looks for some predefined log message formats and turns them into structured data, which surprise surprise, compresses much better. Honestly it seems like you could accomplish the same thing with logstash, fluentbit, etc. by defining a parser and turning messages into json. Any moderately intelligent backend should be able to compress the data that looks largely the same pretty effectively.
As I understand it, a logstream has a fixed schema that’s inferred from the first event that posted to it. Usually not all log events are structured the same way and the schema evolves over time. How is that handled here?
Underlying architecture does support evolving of the log schema. To keep things simple we have disabled this for now, but we can enable such scenarios as / when needed.
I’m working on something similar (log-store.com) and built a database to get around the issue of most databases and file formats requiring a schema… that and I like databases :-)
Relaxing a bit for my one week of unemployment before starting a new job. I got sidetracked in a MTA subway arrival time server written in Java, so now I’m working on a simple build system. Basically the goal is to have a TOML config for the basics and a
Build.java
file for any extra build nonsense. I think this should cover most of my personal needs. Also I’m using some module magic to allow multiple isolated versions of the same library to avoid jar hell.I know one of the cofounders of the project, it’s cool to see it posted here 🙂
This the opposite of my experience, but our workflows are different. I’ll just note some points here:
I have learned to love Zig’s source code (I mean the standard library, not the compiler). It’s very easy to read (for me, at least) and I’ve found because of the explicitness of the language design, I don’t need archaic hidden knowledge to understand what the source is doing. As a result, I’ve stopped searching for answers online and have embraced the code as the source of truth. I’ve gained a better understanding of what’s going on as a result as well which is a bonus.
I don’t quite agree with this. The explicit namespacing for files gives you a tool to make logical separation in your directory. I’ve found Go’s implict namespacing confusing because when I’m reading other people’s code, I never seem to know where to find a symbol (I’m not considering external tools like lsp or grep which I need in Go, but don’t in Zig). In Zig, I can just see which import the symbol refers to. It makes nice and clear.
Again, maybe it’s my workflow, but I’ve found having the tests right beside where the code lives gives me the best tool to learn what the code does, and it never goes out of date. Taking a look at e.g. http server code in stdlib, you can read the code and then how it’s used, and that’s all you need to do. No praying, grepping, hoping, aligning the stars, searching online, banging head against the wall, to maybe find how a piece of code is used. In fact, this is why I’ve started reading the code, because it’s so easy and takes so little time to read the code and see how it’s used.
I encourage you to consider this workflow. I don’t use this flow with any other language because it just doesn’t work: too many redirections, implicitness, and fluff get in the way and you need advanced tools. In Zig, it’s all there, right there, in front of you, nicely collected together. No need for tools. This workflow has been so effective thanks to Zig that I haven’t felt the need to setup lsp yet! I just don’t feel like I need it.
I do something like that, but with LSP, that takes me right to the source/tests. Works offline, no need to even start my browser.
It also leads me to discover better ways to do what I wanted to do, because I spot a function in a module somewhere. E.g. seeing I can do
bufferedReader(reader)
instead ofBufferedReader(@TypeOf(reader)){ .unbuffered_reader = reader, ... }
.That said, different things work for different people, and I hope the docs will one day be as enjoyable as the language.
Having only played with Zig, I agree with this. I prefer tests alongside the code. Drifting the topic a bit, I very much like D’s support for unit tests in the same file as the code and that they can produce documentation.
+1 it’s great in Rust as well. I also like the ability to test module-private functions (I know some people think this is bad). Java is such a pain needing to have a mirrored folder structure for the tests just so things can be in the same package.
Arrived to test-right-beside-code independently when working on Next Generation Shell. I have two groups of tests. One is basic language functionality, it goes separately and is practically not used anymore. The second is for the libraries, where each test lives just below the function.
I’ve noticed that the tests below the function are almost identical to examples that are given in documentation which is just above the function. This needs to be addressed.
No option for third-party sleds?
That would be counter to their whole value prop. The entire system is meant to be tightly integrated and fully turn-key.
I mean the software and hardware specs are open source AFAIK so you could make your own sleds in theory.
I find that I have this attitude a lot more now in code reviews. I’ll see something and I’m pretty sure there’s a better way to structure things, but taking the time to figure that out, explain it, potentially argue about it, and then re-review the updated code is a lot of effort on both my and their part. Especially if their code is functionally fine.
I have side projects to give a fuck about doing things the “right” way 🙂
The text format is nice, and having a schema is great.
The downside, of course, is that you’re using protobuf. Protobuf is amazing – it shows how much code you can squeeze into a relatively small amount of functionality. And it churns surprisingly quickly. Especially when you consider how little the wire format changes.
Not necessarily defending protobuf here, but what other formats are not dependent on field names? That’s one of its biggest pluses imo
I think you replied to a different comment than you meant to. Probably https://lobste.rs/s/f37hri/ascii_protocol_buffers_as_config_files#c_y7jsjt this one?
To be fair, it the wire format changing slower than the code churns is a good thing for a library that implements (de)serialisation. ;)
Statically-enforced types are annoying when you don’t know what types you need and you want to explore the solutions with code.
A great thing about Ruby is you can use hashes and strings to figure out what you need (same with JS objects) and then create actual types once you’ve got some code working that has allowed you to understand the problem.
With stuff like Java or Go, you are fighting the compiler instead of understanding your domain.
I have the opposite experience: Whenever I’m using Rust, SML or Haskell, I always start with defining my datatypes, and use those to understand my domain. The rest of the code flows naturally from there.
In other words: I explore solutions using datatypes.
Same. And if I don’t know what type I’ll need (like u64 or i32 or whatever), then I can use an alias or wrapper type like in the OP so I can change it for every usage in one location. Starting with types helps me build a model of the problem domain before writing any code.
This idea that types make you “fight the compiler” makes little sense to me. On the contrary, they enable the compiler to help me ensure that what I’m writing is correct from the start, free of needless debugging of runtime problems that types would have prevented.
What I’m referring to here (and should’ve made more clear) is exploring a user experience. With something like Rails, I can create a user experience very quickly, and have it using real data, real third party APIs, etc etc. This requires extremely fast iteration, often making drastic changes to see how they feel or how they work. Static typing would introduce two new steps that aren’t providing value for this particular activity: explicitly defining types, and requiring that the entire app conform to all type checks.
These two steps seem absolutely valuable in production, but for prototyping in an existing app, iterating on user experiences and design, they provide negative value and make the process harder.
Depends on what language you use. Most MLs have type inference, even Rust.
When you’re writing something in a dynamically typed language you also need to ensure that your types match up, you just won’t know if they do until you run the code. Trivial example,
3 + x
, werex
is a variable containing the string “foo” will cause an exception at runtime in Ruby. I think it’s reasonable to argue that having that check happen at compile-time makes prototype development faster.I wonder to what extent the difference between the two sides here is a difference in perspectives between (Web or other GUI) app developers and non-app developers.
I never thought to use type aliases that way. Very clever!
I often use that as a first step toward introducing stronger types in a code base written in a strongly typed language by developers who haven’t yet taken to actually using the stronger type system.
Sometimes after adjusting the datatypes, I want to test one function. I know I’ll have to update everything eventually, and I appreciate the compiler’s help with this, but I’d rather try running one example end-to-end first. I shouldn’t need to update code that won’t be used in that example.
I think
-fdefer-type-errors
is supposed to achieve this, and when I used Java and Eclipse it could do this. I could run the project without fixing all the red squiggles; it would just turn them into runtime errors.Like the article says, it does not pay for itself. Ruby “domain code” in the wild is full of type errors, nil references and preventable bugs. I did an analysis recently and over 70% of all our exceptions that led to 500 errors were things a type system would’ve caught. This wasn’t a surprise, all projects I’ve ever been had this class of errors.
There’s little point in writing domain code quickly but faulty. We really need to disavow ourselves from this notion that static types are 100x slower to write: they’re not. You might throw something together in a day in Ruby, but it takes 2 days in Go or something else. This 1 day difference will not make your product fail, specially since you have taken the time to prevent some invalid states along the way.
This is coming from someone with 16+ years of Ruby experience, I don’t dislike the language at all. “Fast to write” is just not a good measure of quality.
See https://lobste.rs/s/z6lpqg/strong_static_typing_hill_i_m_willing_die#c_gfptir for more elaboration on what I’m talking about. It’s not “go quickly from zero to production code”.
Pity that domain code might be dynamically typed. If we have a DSL for our domain, the DSL compiler knows a lot, and thus is in the best position possible to enforce a crapton of invariants up front. I reckon those static checks aren’t free to implement, but they’re likely worth it. Else why are we paying the cost of a DSL to begin with?
Go, while statically typed, is arguably not strongly typed. It’s firmly in the bottom of the uncanny valley of programming languages, which can neither handle nor prevent errors. Been meaning to blog about that …
I’d be interested in hearing why would you’d say it’s not strongly typed
Duration
s to get aDuration
Newer versions of Go do have something close to enums. You can define something like this:
This will disallow implicit casts from other kinds of integer, which prevents other kind of integers being used by accident. The mildly annoying thing is that these are not namespaced in any way within a package so you can’t have two enumerations with the same value (just like in C, something C++ fixed long ago).
That’s not really an artefact of the type system, that’s just the standard library authors getting their units wrong.
What about something like Typescript? You can still create arbitrary objects that lets you explore, but you still have static typing. The type inference removes most of the overhead of writing types as well.
I used to think that, but I’m leaning away from it now. I would say that whatever language you use, 75% of your code ends up having very simple types, so there’s no real gain from having dynamic typing. Dynamic typing is bad for exploratory programming with these because if you decide that the arguments should be c, b, a instead of a, b, c, you can’t use an automated tool to fix it. You do save the time of declaring the types up front, but it’s only a net gain if you happen to get it right the first time.
Where dynamic typing is useful is when you have a complicated type relationship that would be a pain to spell out. These don’t happen often, but when they do, then dynamic looks better. In a dynamic language you might say f can take an x, a y, or a z, but in static, you just say f takes an x and write a y to x and z to x converter or add fy and fz methods. And that’s a relatively simple case.
The other thing about static typing is that it sometimes buys you better performance. That doesn’t apply to TypeScript, but it is nice to have when you have it.
This is factually incorrect. Not only automated refactorings like this exist today. But the earliest example of automated refactorings that I know of are for a dynamically-typed language, Smalltalk.
Note that this is not directly related to the type system of the languages. But whether one uses runtime reflection vs static analysis.
When I say “can’t” I don’t mean “can’t with any possible amount of effort.” I mean “I can’t in the dynamic languages I use today with the tools available to me now.” If you have a tool that can detect signature breakages in Python and JavaScript without using static type annotations, I guess I’d like to add it to my tool chest, but for most people, the way they do it is by adding static types and using MyPy or TypeScript to catch the breakages.
The discussion is about typed vs untyped, so it seems sensible to me limit the discussion to things that are inherent differences. Not things that are different between ‘popular’/‘mainstream’ typed vs untyped languages.
But even if we move the goalpost, to things I can do in Python or JS. That is still factually incorrect. Take PyCharms for example: https://www.jetbrains.com/help/pycharm/change-signature.html
Changing the order of parameters is a pretty low bar.
Without type annotations PyCharm thinks everything is of type Any and it really quickly loses its ability to do interesting analysis or refactoring.
I don’t understand why you think automating this refactoring is so difficult in dynamically-typed languages. Or why you (implicitly) think it would be easy in statically-typed languages.
Suppose you have a function in a hypothetical statically-typed language, which has the following signature:
And you want to change the order of the parameters as stated in your comment:
If you believe an IDE should be able to perform this refactoring correctly, why do you believe it would not be able to perform the same refactoring correctly in a dynamically-typed language?
Runtime construction of dynamic variables and factory types.
I still don’t see the issue.
The stated problem is a refactoring which changes the order of the arguments from a, b, c to c, b, a. If this is something that tooling for statically-typed languages can handle, then it must also be something that tooling for dynamically-typed languages can handle. Effectively, I’m saying that if the tooling can perform this refactoring for:
then it ought to be equally able to perform this refactoring for:
since that is, in essence, what people like to claim a dynamically-typed language really is.
A way to illustrate my point,
globals()["my_var"] = new_value
. How should a refactoring algorithm detect you’re doing something like this with arbitrary strings, or what I’ve actually seen isglobals()["var" + dynamic] = new_value
?I don’t understand how this is relevant to the actual hypothetical posed.
Also, I don’t understand why you think runtime modification is somehow only a thing in dynamically-typed languages, because it very much isn’t, and nobody suggests that it’s impossible to refactor, say, Java codebases when they rely on tons of runtime configuration, runtime injection, etc. making it impossible to know ahead-of-time which code paths might execute or with which values.
Just use a union/sum type.
I finally bit the bullet and made a separate typed AST for Sasquach. I finished updating the type checker to create the TAST, now I need to plumb it through to a couple places so I can actually use it in the bytecode generation step and get rid of all of the map lookups I’m currently doing.
Ever since seeing this post about using preconfigured templates for log compression, I’ve been wanting to automatically detect log message templates. Lo and behold I just found this repo that has several algorithms to do just that, hopefully I’ll actually be able to incorporate one of them.
Not exactly a hot take, but I think virtual threads will largely spell the death of the reactive programming style in Java. Of course there are problem spaces where its conceptual model is a better fit than a thread-centric model (e.g., because backpressure is a first-class concept) but from what I’ve observed, the vast majority of people who are using reactive libraries are looking to support large numbers of concurrent clients and are tolerating the reactive model to achieve that goal. Virtual threads will be a much better fit for those people.
Of course, this won’t happen overnight, but I’m guessing with the release of Java 21, we’ll see a sharp drop in the number of new reactive projects.
Agreed. I maintain Manifold, an old streaming/CSP-style lib for Clojure, and a year ago, a fellow stream lib implementer and I were discussing the way forward with vthreads, what to do about backwards compatibility, etc.
And yeah, one of my conclusions was that if vthreads existed then, Manifold would probably not have been written, it wouldn’t have made enough sense. Vthreads cover much of it, and structured concurrency will cover most of the remainder.
Netty, and other event-driven servers have similar situations. Anything that revolves around managing a thread pool, really.
Java, the language, is built for blocking I/O. This never changed and you can see it in how its syntax requires blocking I/O. Examples:
Java with reactive stuff is basically using a Java subset, and isn’t in the language’s character.
OTOH, “reactive” is really an euphemism for function composition, which will never go out of fashion, but which always strikes fear in the hearts of developers, especially when the word “monad” gets used.
I agree with your general sentiment. This Loom feature brings Java closer to Erlang. Java will still be missing the monitors, the nodes (so the Erlang’s OTP), but the VM were a function call can spawn a thread, and have the function executed in that new thread – will probably bring more of Erlang-style idioms into Java going forward. And that’s a good thing.
It’s not exactly the same but with the structured concurrency API it gets even closer
Continuing to refactor a bunch of code in the Sasquach compiler so it’s easier to maintain going forward. I replaced a bunch of uses of Strings with Java’s
ConstantDesc
classes and I’m gonna replace my crappy dispatch code with dynalink.I might try to write a short post about how awesome dynalink is.
I personally would be weirded out if I received automated messages like these. If you really want to stay in touch with me, why not have a recurring calendar event? If I had an SO who used this to say “Miss you” at random times, it would feel meaningless to me. It would feel a lot more meaningful if there was a typo and I knew it was written deliberately—until the bot gets smarter and inserts random typos.
Oftentimes, when the app reminds me to reach out, I’ll write whatever I’m feeling at that moment and not use one of the templates. Other times I’ll be busy with something and one of the templates is spot on with how I’m feeling. I tried using recurring calendar entries in the past, but there wasn’t enough there to keep me consistent.
Based on the feedback here, which I agree with, it might be better to have suggestions for topics to message about or prompts that could remind you about something about that person rather than a full on prepopulated message. E.g. “Ask them about their latest travels” or “Message Bob Smith about his favorite hobby “.
This is a great writeup! I thought about going down the same path with my language, but it appears you got much further along 😀 I haven’t thought it through too much, but for the Ord case could you add an implicit Eq parameter to signify the dependency?
On another note, I also wanted to have something like derive for these modules but I’d like to avoid a whole other syntax for macros. I was thinking of something like comptime specifically for that but still have first class generics.
I thiiiiink so, but depending on how it’s done the compiler might have a tough time proving that two different implementations of
Ord(T, Eq(T))
actually lead to the sameEq
value? I’ll have to think about it more, probably with more coffee in my system.I definitely intend to have some sort of macros or comptime code generation in Garnet, but yeah the syntax is a bit of a bear. I actually find Rust’s template-y macros pretty sensible most of the time, once you climb the learning cliff of figuring out how args and repetition works, but there’s reasons I haven’t tried to tackle macros for Garnet yet. :-)
Personal: try to actually deploy my pivoted version of Eventlandr this week. The new version is centered around figuring out which friends are available rather, than planning a specific event ahead of time. I need to shore up the Vite production build of the JS code and ensure the backend serves it properly and create the Dockerfile for the Fly.io deployment.
I might blog about the idea for the site itself or how I sorta built a custom Java framework centered around server-side rendering.
Drone for your first one? Bob?
I should clarify that by “self-hosted” I mean “by organizations employing teams of engineers” and not “by individuals”.
While easy to use software is possible, I reckon making it is quite hard.
Netdata?
At #PreviousJob, we used BuildKite, and it was fantastic - you could deploy the agent pretty much anywhere you wanted, and I found the configuration much easier than any other system I’ve used (such as GitHub Actions).
I recently realized that what I really want out of a CI system is running locally. Ideally, with decoupled backends, so, it can run tasks in containers, but also just processes, for simpler projects.
Most CI configuration languages don’t really let you do that, so you need to duplicate the commands for building, linting, testing, etc.
There’s that terra something tool, but it requires containers always, I think.
I had a couple (very) rough ideas on a CI system. One was to make the actual task configuration a Lua script with a bunch of predefined functions to make a declarative pipeline possible, but to also allow the user to drop into more imperative steps. Using Lua lets you more effectively sandbox the tasks than using something like the JVM, the runner could be much leaner, and users could possibly get real autocompletion for their steps instead of only relying on docs for some made up yaml DSL. I also really want to integrate it more deeply with metrics, so you can see annotations in Grafana when you deployed and have automatic rollback when something goes wrong.
(nods) Though something like this remains, to me, the most ideal architecture.
Integrating webauthn into my social webapp to enable registration without needing an email or phone number. The once ios safari web notifications arrive, I’ll be able to have a full app without PII.
I saw this before, it looks for some predefined log message formats and turns them into structured data, which surprise surprise, compresses much better. Honestly it seems like you could accomplish the same thing with logstash, fluentbit, etc. by defining a parser and turning messages into json. Any moderately intelligent backend should be able to compress the data that looks largely the same pretty effectively.
The columnar storage aspect also contributes greatly to the increased compressibility.
As I understand it, a logstream has a fixed schema that’s inferred from the first event that posted to it. Usually not all log events are structured the same way and the schema evolves over time. How is that handled here?
Underlying architecture does support evolving of the log schema. To keep things simple we have disabled this for now, but we can enable such scenarios as / when needed.
Does ingest require a predefined schema? If not, how do you handle converting the schemaless data into Parquet?
I’m working on something similar (log-store.com) and built a database to get around the issue of most databases and file formats requiring a schema… that and I like databases :-)
Heh I’m also working on a schemaless log ingest and search tool written in Rust, which is why I’m asking.
There is a MAP type and also a JSON/BSON type that would help. Maybe that’s what they use?
Some context that I missed initially, on Windows a named mutex is actually used to synchronized across processes rather than threads.