Crystal currently provides a few ways of dealing with lazily evaluating code. The most common being the block getter macro. It’s also pretty easy to use captured blocks to delay object instantiation until first accessed, especially via forward_missing_to. E.g.
struct LazyObject(T)
forward_missing_to self.instance
# :nodoc:
delegate :==, :===, :=~, :hash, :tap, :not_nil!, :dup, :clone, :try, to: self.instance
getter instance : T { @instantiated = true; @loader.call }
getter? instantiated : Bool = false
def initialize(@loader : Proc(T)); end
end
The main challenge/annoyance with this is the underlying code has to be aware of the laziness. E.g. you have to type everything as LazyObject(Article) vs just as Article, even if the former it totally compatible with the latter (of which [RFC] Standardized Abstractions is a similar use case).
However I recently came across this new PHP feature for Lazy Objects and one part really jumped out at me.
[lazy ghosts] are indistinguishable from an object that was never lazy
What this ultimately means is if you have a function like:
function test(Article $obj)
You can freely pass both a lazy ghost Article or an actual Article instance and it treats them both the same. I’m not sure how/if something like this would be implemented in Crystal land, but would be a very useful thing to have. A solid use case being for ORMs:
# pseudo code
class User
getter! id : Int64
getter name : String
end
class Article
getter! id : Int64
getter author : User
end
article = Article.find 123
# `Article.author` could be a `User` ghost that is aware of its PK
article.author.id # => 456
# Accessing other fields lazily initializes the ghost which actually performs the query to hydrate the rest of it
article.author.name # => Foo
My current, somewhat naive thinking, is that if we had a say Ghost(T) type in the stdlib, that the compiler could just know Ghost(Article) is compatible with Article. Which just isn’t something you can do solely in user code.
But curious to hear other’s thoughts!