Find a better type structure
Currently, we have:
- A ton of extension interfaces: CheckedDatastore, ScrubbedDatastore, Batching, etc...
- A ton of wrappers: Delayed, Mount, Sync, Autobatch, etc...
And unfortunately, every wrapper needs to implement and forward every extension interface to the underlying datastore. It's kind of a nightmare.
According to @bigs, however,
Typeclasses and modules compose pretty cleanly stuck_out_tongue! Monad transformers exhibit that kind of extensionality pretty well!
I'm not sure about languages like Haskell but I'm pretty sure this can't be done in most languages. However, it's worth discussing as it would be great to have a better system.
Basically, what I want is (pseudo-rust):
trait Datastore { ... }
trait GCDatastore: Datastore { ... }
struct MyDs {
...
}
impl Datastore for MyDs { ... }
struct Delayed<D: Datastore> {
inner: D,
delay: Duration,
}
impl<D: Datastore> Datastore for Delayed<D> {}
// Can't express this:
forall<X> auto impl X for Delayed<D> where D: X via self.inner;
// That is, auto implement all traits implemented by D on Delayed<D> by proxying to self.inner.
This kind of behavior is usually achieved via the Deref trait. In rust, if a type X implements Deref<Target=SomeOtherType> and someone calls some_x.foo(), the compiler will first try to lookup foo on X, then on SomeOtherType, recursively (if SomeOtherType also implements Deref). However, this doesn't mean that the type X actually implements any of the traits implemented by SomeOtherType.
Thoughts?
One solution is to define new types and use go composition:
type Datastore interface {...}
type GCDatastore interface {...}
type MyDs struct {...}
// implement Datastore and GCDatastore for Delayed
type Delayed struct {
Datastore Datastore
Delay Duration
}
// Only implement Datastore for Delayed
...
// Compose them:
struct{Delayed, MyDs}{Delayed{Datastore, someDelay}, MyDs}
Yeah, this seems pretty solid. I just kind of wonder if it breaks the facade of our interface altogether... Without generics you're going to have to have a direct reference to the underlying struct unless you're willing to resort to type assignment, which I think is probably OK. Like anything, it will have to be wielded responsibly.
By "type assignment" I assume you mean "type assertion" (what it's called in go)? We do that anyways. Basically, I want to be able to take some object of type Datastore (where Datastore is an interface) and cast it to, e.g., GCDatastore).
Unfortunately, my method won't work with, e.g., the mount datastore (or any dispatching datastore, really). This is really where dynamic languages shine.
This article describes the situation well: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html
I am interested in this because I want to make a dispatching datastore that proxies reqs over a network. With the current type layout, this isn't possible to do dynamically because we can't dynamically define methods. I'll experiment with codegen, which will require generating 2^7=128 different implementations (datastore, batching, scrubbed, checked, persistent, gc, ttl), and would remain backwards compatible.