Hacker News new | past | comments | ask | show | jobs | submit login
Representing State as Interfaces in Go (emoses.org)
23 points by todsacerdoti 12 months ago | hide | past | favorite | 16 comments



This seems similar to replicating substructural type systems like linear or affine types via interfaces.

I can't remember who gave this talk, but a prime example is a `File` type. A `File` type shouldn't have a Read() method until the file itself has opened. And calling `Close()` on the File should prevent further reads. Substructural typing - and this pattern - allows you to refine types based off of the type's instance state.

There's a ton possible here but I haven't really kept up to date with type systems in the last 5 years. Stuff like this has probably advanced a ton!


Wouldn't the instance state here be "was constructed"? That is: you should always be able to call `Read` because the constructor opened the file, and you shouldn't have to call `Close` anyway because the destructor did.


I suppose it depends on the API. Some languages require grabbing a handle and calling open. Either way, theory is the same!


I thought this was going to be a full state machine, where each state has a different interface, but it looks like the Resolved state is just the result of Resolver.Execute(). Wouldn't it be preferable to return the result in a new object, rather than reusing the same object?


This reminds me of the Make Illegal States Unrepresentable[0] idea, where something will return a ‘ResolvedExecutor’ type to guarantee that you’re only working with something in the state you want. Go makes this a little clunky to do, though. I suppose you could use Generics to implement a Preresolved and Resolved struct.

[0] https://fsharpforfunandprofit.com/posts/designing-with-types...


You start with a Resolver, call Resolver.Execute, and up with a Resolved (which happens to be the same object, but that's really an implementation detail). But the type system doesn't prevent you making further calls to Resolver -- that's one of the illegal uses we'd like to prevent, if I understand the example.

It might work in a language with linear types or some concept of ownership. Execute() would take ownership of the Resolver and prevent it being called again.


> Wouldn't it be preferable to return the result in a new object, rather than reusing the same object?

It wouldn't make much sense. The whole point is not needing a new one, but reusing the record.


Maybe I misunderstand the example, but in what way is that "the whole point"? You can implement it that way, and maybe save a tiny bit of memory (but Go has a great garbage collector so it doesn't seem very significant). But it could also cost memory if the pre-resolved object has state that isn't needed after Execute() is called.

The given example looks like a Builder pattern. Although that name smells strongly of Java, so Go users might prefer to avoid it. :)


> Maybe I misunderstand the example, but in what way is that "the whole point"?

I wanted to express that having functions

   f(a) -> b {}
   f(b) -> c {}
   f(c) -> d {}
   ...
would be quite trivial.

I would say that the whole point of the builder pattern is _not_ having an ordering of the "chained" functions.


Isn’t the point of the builder pattern to separate the builder object (which is mutable) from the final built object (which can be immutable)?

That seems like the same thing as the example in the original post.


For me a "builder pattern" is also something like this with immutable data:

   built = empty().configA(a).configB(b). ... .configZ(z)
  
where each

   config?(this : T, k : K) -> T
For me the defining feature of a builder is the (possibility of a) gradual "configuration" (or whatever) of the data structure being "built". A state change is something else in my opinion, but of course, the difference between a "state change" and a "configuration" (which does change state) isn't a clear one, so if for you that is still a builder, that's fine for me too ;).


I don't have a good name for it, but I used the same pattern 20ish years ago for game engine stuff.

Java folks might have a name. It's similar to the Foo().bar().baz() constructor pattern.


I think you're thinking of the Builder pattern. That's a bit different, because you return the same type and same object on every chained call.


No, I think it is the same thing. Typically¹ with a Builder there's one final method that builds and returns the final object (which has a different type).

¹ Edit to add: well, it's a pattern I've seen a lot, anyway. I think it's preferable to mutating the final object in place because it "makes illegal states unrepresentable" as others in this discussion have mentioned.


A possible match would be the Typestate aka Type State pattern.

For some reason I only see Rustaceans talking about it, though it's perfectly applicable in any language with static typing.


If you want to use this pattern, you'll probably end up with a lot of duplicated code e.g. for SQL query builders. I could imagine that Rust makes this kind of pattern easier due to traits and macros. A lot of languages do not have these features.

And then of course Rust people care about correctness more in general.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: