Extracting singleton functionality into modules

Hi there,
I am new to Crystal, but a long time Ruby fan.

In Ruby, we have the Singleton module.
https://ruby-doc.org/stdlib-2.5.1/libdoc/singleton/rdoc/Singleton.html

I tried to implement the same thing in Crystal.
I started with writing a class with the singleton property.
The class works just fine.

  class Klass
    private SINGLETON = ::Mutex.new
    @@instance : self?

    def self.instance
      @@instance || SINGLETON.synchronize do
        @@instance ||= new
      end
    end
  end

However, when I tried extracting the functionality into a module, I’ll get a compilation error.
Could you show me how to do it properly? Or if this is possible at all?

One thing I like about Ruby is people always make a trade-off on type-safety with convenience and usabilities.

I do it like this:

module Singleton
  macro included
    private SINGLETON_LOCK = Mutex.new

    def self.instance : self
      @@instance || SINGLETON_LOCK.synchronize { @@instance ||= new }
    end

    macro method_added(m)
      \{% if m.name == "initialize" %}
        private def initialize
          previous_def
        end
      \{% end %}
    end
  end
end

Constants are guaranteed to be initialized only once, so instead of a class variable, you could just use a constant (being a signleton it won’t need to be reassigned anyway) and don’t need to do mutex synchronization. The runtime already takes care of that.

So the implementation of a singleton is actually quite trivial. IMO there’s no need for a generalized module to include this, when it’s just three loc:

class Foo
  INSTANCE = new

  private def initialize
  end
end

OFC you can still inherit this type, or call #dup/#clone to get a copy. If you want you can also restrict this. But I don’t think that’s strictly necessary. The important thing about a singleton is to not expose a public API for directly creating a new instance (.new).
Another alternative to a singleton instance is simply using a module.

module FooSingleton  
  class_property foo = true
end

FooSingleton.foo

Btw. there are already quite a few discussions on singletons: https://github.com/crystal-lang/crystal/pull/1399, https://github.com/crystal-lang/crystal/issues/8846, https://github.com/crystal-lang/crystal/issues/718

4 Likes

Thank you
I learned something from both answers.

However, using constant might not fit the bill in some situations.
For example, in my case, the initialization is a binding to C object initialization; the operation of the C library is a side effect of class initialization.
The constant approach won’t work because the side-effects would always occur.
In contrast, singleton instance can be lazily evaluated.

Someone already made the point in the issue.
Now the Mutex is already in the core. Do you guys still work on the singleton module?

class Foo
  INSTANCE = new

  private def initialize
  end
end

Oh, that looks nice. But how does that work? In an interpreted language, that would create an instance as soon as loading the file, something that one would want to avoid. But I’m guessing that the compiler would notice if one didn’t use that constant, and optimize the initialization away, or?

Correct, code that isn’t used/referenced anywhere is not used, so in this case the constant wouldn’t be compiled.

1 Like

Hi, How you extract the functionality into module? could you please give code sample? And, what is the compilation error?

AFAIK, you code should just work.

module M1
  private SINGLETON = ::Mutex.new
  @@instance : self?

  macro included
    def self.instance
      @@instance || SINGLETON.synchronize do
        @@instance ||= new
      end
    end
  end
end

class C1
  include M1
end

c1 = C1.instance # => #<C1:0x7fb698e1be60>
c2 = C1.instance # => #<C1:0x7fb698e1be60>

Although, as said by straight-shoota, code can be reduce to: (ignore make initialize private)

module M1
  @@instance : self?

  macro included
    def self.instance
      @@instance ||= new
    end
  end
end

class C1
  include M1
end

p c1 = C1.instance # => #<C1:0x7fb698e1be60>
p c2 = C1.instance # => #<C1:0x7fb698e1be60>

AFAIK, the class variable in Crystal(basically, the ruby equivalent of class instance variable, right?), can be guaranteed to be initialized only once for includeded class?(check following example) could you please give a example for initialized twice? thanks

module M1
  @@instance : self?

  macro included
    def self.instance
      @@instance ||= new
    end
  end
end

class C1
  include M1
end

class C2
  include M1
end

p C1.instance # => #<C1:0x7fb698e1be60>
p C1.instance # => #<C1:0x7fb698e1be60>

p C2.instance # => #<C2:0x7fb698e1be70>
p C2.instance # => #<C2:0x7fb698e1be70>

I have one more question hope the core team can answer me.

Why we can introduce (very specified) module like OAuth Client into standard lib, but, instead, a minimized implementation Singleton pattern could not be added?

Because it’s unnecessary:

No, it is not trivial.

  1. tree loc not limit call on initialize/new method.
  2. you can still call #dup, #clone.

reference the ruby version singleton, there are more work to do.

The key point is, use singleton module just like Ruby (single included it) is a best practice, we should inherit Ruby goodies.

2 Likes

Sure, it’s nothing fancy. Just a trivially minimal singleton implementation. That’s good enough for many use cases where people have asked for this.

I expect a more sophisticated implementation would be accepted into stdlib. There is an open feature request for this: Singleton module · Issue #8846 · crystal-lang/crystal · GitHub
However, little response to that issue indicates not a lot of interest from users. So nobody has picked it up yet. Contributions are welcome, but it’s not a priority for the Core Team.

4 Likes

It’s a philosophical question, innit?

While a more correct implementation that, for instance, prevents duping, can be considered more “pure”, what a lot of people are probably looking for when they say “singleton” is just a class that makes it easy to get that single instance without too much faffing about.

In that light @straight-shoota’s example is simple and elegant.

Of course, that doesn’t help much if you really want to make sure that nobody ever clones your INSTANCE. I guess it could become a problem in big projects.

But one thing I’ve learned is that you don’t always need the proper textbook implementation of “a thing”. Often something simpler but with limitations works just as well, and can make things simpler. Sometimes you just extract data from HTML with regexps.

1 Like