Understanding visibility, immutability, and package management

There is no perfect language, we all know that, but I really want to have Odin as my main language as I think it’s very close to the way I think about applications. I like a lot the mentality behind the language but there are a few things that are setting me off. I would like to understand better the rational for those decisions and or if there are any good links and posts that I could read about it.

  1. Visibility
    It’s hard to make things private. I’m the kind of person that likes to expose a small API interface and have many private functions and structs that support that small API. In my opinion it makes it simpler to reason about libraries where you don’t need to deal with internal parts of it.

  2. Immutability
    This helps with the idea that once something is created, the state will remain the same and with that it will know that it was not tampered along the way.

  3. Package manager
    I remember seeing a video where Ginger Bill talked about why he don’t like package managers, and I his points are totally right, but why not some small tool that comes with odin to make it easier to vender dependencies, list what is included, their versions, etc… like Go that fetches the dependencies directly from the source code / origin.

  1. You can make a whole file private with #+private before package declaration. https://odin-lang.org/docs/overview/#private. As a side note, I’ve found that the need for private procedures is mostly infrequent. I will define procedures I wish to keep internal inside exposed procedures when needed and where possible. If it’s a procedure I use more than once in other procedures and needs to stay internal, then it gets defined on it’s own, but with @(private) or @(private="file").

  2. Immutability becomes extremely clear in a relatively short time through usage. For the most part, when passing to procedures, all variables are immutable. Pointers allow for mutability. There are some exceptions. Karl Zylinski’s book is a fantastic entry point and explains mutability quite well in the early chapters. https://odinbook.com/. On the subject of closures, there’s this thread: how-to-get-out-of-the-closure-mindset, where charles_23195 makes a prescient comment: “If you have no idea what state your program will be in when it gets somewhere, then you probably need to rethink your program.”

  3. After using Odin a while, it’s very clear to me why no package manager or tools are needed. One of the things that helped me get to this point was embracing the notion that by-and-large all the documentation you need is in the source files, vendor or otherwise. The examples found here help https://github.com/odin-lang/examples. Usage of the language got me the rest of the way. If you’ve used other languages, this should come quickly.

Others here in the forums could probably give more concise technical answers. My answers are purely anecdotal.

If you are looking for a deeper understanding of the whys, etc, there’s this: https://www.gingerbill.org/article/ and this: https://odin-lang.org/news/

Update - A few other useful links:
https://zylinski.se/posts/introduction-to-odin/
https://zylinski.se/posts/dynamic-arrays-and-arenas/
https://www.youtube.com/@karl_zylinski
Generic Odin procedures: Parametric polymorphism

1 Like

Visibility

Personal preference.

I had more times than I could count when I had to rewrite completely/partially a library because internals were not exposed and I couldn’t extend outside of it’s source tree to add something crucial and either due to licensing or upstream not accepting the change it was not possible to do in-tree (it’s usually something very-very product specific).

The biggest nope in my book is opaque handles to data structures. Best you can do is copy the struct definition and cast the handle to “see inside” which is long term as reliable as a cotton candy bridge during monsun.

Immutability

Anything that’s not pointer like is immutable when not passed as a ^T to a procedure. If you want actual write protected data something like const ^T will not save you, instead

// allocate sufficient memory
bytes, err := virtual.reserve(size_of(T))
assert(err == nil)
// make it RW
err = virtual.commit(raw_data(bytes), len(bytes))
assert(err == nil)
// initialize it
ptr := cast(^T)raw_data(bytes)
ptr^ = init_value
// make it RO
ok := virtual.protect(ptr, size_of(T), {.Read})
assert(ok)

Now this will actually crash the app when rouge code starts to write to ptr (even if that code is unknown to the compiler). Of course, you can build up very complex data structures allocated into virtual.Arena and simply protect each memory block the arena uses essentially freezing it’s content.

And here if I circle back to the 1st point, if the arena internals would be hidden you wouldn’t be able to easily do this.

Package manager

The most universal dependency manager you’ll ever see is your VCS.

  • probably already has a way to vendor an external source (e.g. git submodule),
  • probably already has a way to automatically fetch the correct version you use (e.g. git submodules update --init --recursive),
  • it can probably also list the current versions you are using (e.g. git submodule foreach git describe --tags HEAD),
  • works with any language
  • and so on…

Worst case you just have to write a couple wrappers to have something short to do the same thing, in case of git you can always add command aliases in your config, or in the build script you use.

The rest is really just “repo hygiene”, especially with Odin. As long as all dependecies goes under a single directory it’s just a single flag to include them all in your project -collection:deps=/path/to/deps and in any file it will be a simple import "deps:foo". This is the benefit of actually having packages defined in the language and not in a separate package manager.

1 Like