6 ms·
I would love to play with a programming language that has a notion of an initialization phase, where constraints are relaxed. So often, especially in cli progr
by richard_todd 6y ago
I would love to play with a programming language that has a notion of an initialization phase, where constraints are relaxed. So often, especially in cli programs, you just want to set up some ambient state based on arguments or whatever that will be constant after you set it up. After setup, then it might be read by multiple threads, etc, and it's totally safe, except you could never convince a language like Rust that it is. Even a series of one-time copies into global "constants" would be sufficient to do the kinds of things I'm thinking about... require it to happen from `main` and (harder to do...) maybe don't let the user create threads until the switch is flipped. But don't put run-time checks on access or whatever... make it truly global and constant once initialization is over.
- heinrich5991 6y agoYou can do lazy_static! initialisation in Rust. > And it's totally safe, except you could never convince a language like Rust that it is. I think you should be able to create a safe abstraction around that in Rust.
- richard_todd 6y agoYeah, I've seen lazy_static. From the documenation [1]: > The Deref implementation uses a hidden static variable that is guarded by an atomic check on each access. That's totally unnecessary in the scenarios I'm talking about, but it's needed to make Rust happy. Also, it's not totally clear to me that I can feed a lazy_static information derived from main() like command-line arguments, though I did not try too hard to figure that part out. I just wound up plumbing arguments through all my functions. [1]: https://docs.rs/lazy_static/1.4.0/lazy_static/ https://docs.rs/lazy_static/1.4.0/lazy_static/
- ChrisSD 6y agoYou want OnceCell's unsync type for that: https://docs.rs/once_cell/1.4.0/once_cell/unsync/struct.OnceCell.html https://docs.rs/once_cell/1.4.0/once_cell/unsync/struct.Once... Assuming you're dead set against thread local storage.
- richard_todd 6y agoIt's not about "dead set," it's just a thought experiment. Setting global constant data at the beginning of a program, before "the real work" starts, is totally safe to do, but there's not a language I'm aware of that lets me explain this to the compiler. Either you get a C-style free-for-all, a Rust-style series of road-blocks, or a managed-language with runtime overhead. Imagine a Rust where a static variable (not mut) could be modified in `main` in a type of unsafe {} block. Then, access to those statics in the rest of the program, even across threads should be safe and incur no runtime checks. That would be similar to what I'd like to try. The unsync type you linked says it isn't thread-safe. If you convert it to a sync type, then it has extra runtime checks. Both sync and unsync make you unwrap Option<T>'s to get the values, which again would not be necessary if I could communicate to the compiler that I am going to set up some global constants, and they will all be ready by the end of some lexical block. Just thinking out loud.
- heinrich5991 6y agoYou can do that in Rust by creating your own abstraction around `UnsafeCell`. Say `InitializeCell`: use std::cell::UnsafeCell; use std::mem::MaybeUninit; use std::ops; pub struct InitializeCell<T> { inner: UnsafeCell<MaybeUninit<T>>, } unsafe impl<T> Sync for InitializeCell<T> {} impl<T> InitializeCell<T> { pub const unsafe fn new_uninitialized() -> InitializeCell<T> { InitializeCell { inner: UnsafeCell::new(MaybeUninit::uninit()), } } pub const fn new(init: T) -> InitializeCell<T> { InitializeCell { inner: UnsafeCell::new(MaybeUninit::new(init)), } } pub unsafe fn init(&self, init: T) { (*self.inner.get()) = MaybeUninit::new(init); } } impl<T> ops::Deref for InitializeCell<T> { type Target = T; fn deref(&self) -> &T { unsafe { &*(*self.inner.get()).as_ptr() } } } static VALUE: InitializeCell<i32> = unsafe { InitializeCell::new_uninitialized() }; static OTHER_VALUE: InitializeCell<i32> = InitializeCell::new(6); fn main() { unsafe { VALUE.init(5); } println!("{}", *VALUE); println!("{}", *OTHER_VALUE); }
- dgellow 6y agoThat's not exactly what you mean, but you could do this in Go by using the `init()` function. From there you assign a package level variable to either an object that only exposes public getters, or one that is only exposed/manipulated by public functions. var myVar MyType func init() { myVar = newMyType() } func GetMyType() MyType { return myVar } init() is run when the package is imported. myVar is constant (though not guaranteed at the type level). GetMyType() is the only way to access myVar and return a copy, not a reference.
- pradn 6y agoThe observation that programs have distinct init and main phases is one reason why capability-based permissions exist. You can do initial work - read a config file, open ports - and then drop the capabilities to do those things. This way, a buffer-overflow that occurs after the init phase ends up being less harmful. The program no longer has the capabilities to open new files or new ports or cause more havoc.
- richard_todd 6y agoYes, or on a more primitive level how traditional UNIX lets a process drop setuid-root permissions once you've done what you need to do with them. In some managed languages, you can define classes with constant members, but obviously they let you write to them during the constructor for the objects. That's the kind of restricted-scope global-state setup phase I am after in an advanced systems programming language. It would be an interesting experiment, anyway.
- kstrauser 6y agoI really like the OpenBSD "pledge" API (https://man.openbsd.org/pledge.2 https://man.openbsd.org/pledge.2). At any point, a program can promise that it will only make specific syscalls from then on, and subsequent pledges can only further restrict that list of syscalls. Once a process has given up rights to open a local file, for instance, it can't get it back. I think that's beautiful.
- mjw1007 6y agoThe Rust RTIC framework for embedded code (previously named RTFM) works rather like that. https://rtic.rs/0.5/book/en/ https://rtic.rs/0.5/book/en/ It's designed for working with interrupts safely rather than threads (so the "initialisation phase" is basically "before enabling interrupts"), but I suppose the same techniques could be used to make something more mainstream.
- baby 6y agoGolang has an init() function just for that
- Mr_P 6y agoDependency injection systems (like Dagger or Guice) are essentially programming languages that specifically model this kind of initialization-vs-runtime distinction. Dagger is actually implemented as a compiler extension. Singletons can be declared once, and then provided to various systems that need it, with read-only access. In fact, they're more flexible than that: one can declare multiple nested "scopes" that each have their own initialization phase. For example, a web server could have one scope for process-wide singletons, and another for request-scoped "singletons". The only thing missing is that these DI systems are for java, which doesn't have the const-correctness you'd ideally want. I'm not aware of any equivalent (and widely-used) systems for languages like c++ or Rust. (Yes, I can feel hundreds of HN readers rolling their eyes as they read this, given the bad rep. DI systems have, but they're widely popular among FANG companies for a reason.)
- pornel 6y agoRust has https://lib.rs/once_cell https://lib.rs/once_cell for this. It's a wrapper type that ensures something can be written to only once, atomically, and after that it's immutable, so it can be shared freely. It's not a program-wide stage, but it's sufficient to use it in `main()` to make it behave like that.