Print out the instance methods defined class? or the path where the current class is?

As we know, in ruby, we have many introspection methods in runtime, e.g. Object#instance_methods, Module.class_methods etc …

As a static language, there may not those methods exists in runtime, but, i consider, print all instance methods/class methods which defined on one class on compile time, is possible, right?

Following is a example:

require "bip39"
require "secp256k1"

m0 = Bip0039::Mnemonic.new 256
priv = Secp256k1::Num.new m0.to_hex
key = Secp256k1::Key.new priv
p! typeof(key) # => Secp256k1::Key

Because LSP support for crystal is poor, there is no easy way to jump to definition when i cursor on the key variable, and pressing some keybinding or click on mouse, i have to open source code to find it out.

so, i consider if there have a more easy way to do this? e.g. consider above example,
I really want to know if there is a easy way to print out where Secp256k1::Key is defined, or even more, print all instance methods defined on Secp256k1::Key directly?

e.g.
how to know a method named private_hex is defined in this key object quickly?

key.private_hex # => "08647a626a6c96bfa9f5847a7d53f78b4cd23234a4b8fa22c489e838af4cb94e"

I’ve been thinking about this a lot these past days.

It’s tricky because Ruby is dynamic so everything is a VALUE and there’s a lot of runtime information that isn’t present in Crystal.

That said, I wonder if it would be nice for Class to provide methods like subclasses, methods, etc., that return runtime values. These can be used to quickly introspect types without having to open a browser or source code to see the API. Or they can be used to maybe generate documentation or transform the code to something else, like a schema. It’s tricky because type restrictions aren’t solved, so for now they can just be represented as strings.

So, the idea would be that Class#sublcasses doesn’t return an Array of Class. That’s kind of impossible. Same with Class#methods, there’s no existing type that we could return there.

But we could introduce intermediary types.

Here’s what I got so far:

https://play.crystal-lang.org/#/r/dfjf

Thoughts?

The nice thing about this is that all of this runtime information is only produced if asked. If you don’t call any of that nothing of that will exist at runtime.

You could also use this in the interpreter to quickly find out about methods and types (I just tried it and all the code runs fine in interpreted mode).

10 Likes

I know @ftarulla wanted to add .subclasses and .all_subclasses (our implementation was more naïve).

2 Likes

Really really Cool !! for those awesome macro.

i am still not deep investigate this code, there seem like exists a typo in play.

so, those type code never be used, right?

BTW: i am curious if there exists a way to always run those macro definition before my project code is start?

Let me still explain use Ruby anyway, step by step.

  1. assume i create a file, /home/zw963/foo/mixin_helper.rb, it content like this:
module Kernel
  # view a object mixed in module.
  def mixed_in
    if self.class == Class
      ancestors[1..-1] - [BasicObject, Object, Kernel]
    else
      singleton_class.ancestors[1..-1] - [BasicObject, Object, Kernel]
    end
  end
end

as you can see, i define a method, for check all mixed class for a class or object in ruby.

  1. I add this file path into $RUBYLIB environment variable in my login shell, e.g. .bashrc.
RUBYLIB=$RUBYLIB:/home/zw963/foo

it same as use this ruby command line arg:

-Idirectory     specify $LOAD_PATH directory (may be used more than once)
  1. I set another env, $RUBYOPT, as: -rmixin_helper.

it same as use this ruby command line arg:

-rlibrary       require the library before executing your script
  1. After i done those steps, mixed_in methods was injected into any Object instance before my project start, i can always use like: SomeClass.mixed_in or some_object.mixed_in

Hope i explain clearly for this, if instead use Crystal, what i want is basically same.

I want to run all the code in https://play.crystal-lang.org/#/r/dfjf , from line 1 to line 187, before start any crystal program, as such,
i can always use pp! Foo.methods in my project, right?

if this is possible?

Thank you!

1 Like

It’s possible if we include that functionality in the standard library, out of the box.

There’s no easy way to inject code to all programs. Crystal is not as flexible as Ruby here, and I think I prefer less flexibility here.

I also had a typo in my code but since it’s not called it’s not caught.

2 Likes

Yes, me too, though use years, but i really don’t like ruby on rails, for this reason, i really don’t like class_eval, eval, and dynamic arguments forwarding in ruby (e.g. optional parameter + options hash), even, pry is not so helpful in some cases, Sequel + Roda is better, the code is really robust, beautiful (as Crystal), But, when search pattern in source code use tools rg for Crystal project, is almost painless than Ruby Project because Crystal less flexibility.

Hi, @asterite , any update for add those macro methods into Crystal std-lib?

No, I’m not the one to decide those things.

Hi, @asterite , To make the above macro more functional, I added superclass and superclasses methods, like this:

def superclass : Class
    {% if @type.superclass.nil? %}
      Object
    {% else %}
      {{ @type.superclass }}
    {% end %}
  end

  def superclasses : Array(String)
    a = [] of String

    superklass = self.superclass

    until superklass == Object
      a << superklass.to_s
      superklass = superklass.superclass
    end

    a << "Object"
  end

But, as you can see, I have to return a Array(String) instead of Array(Class), because, If i change code to this:

def superclasses : Array(Class)
    a = [] of Class

    superklass = self.superclass

    until superklass == Object
      a << superklass
      superklass = superklass.superclass
    end

    a << Object
  end

I will get following compile-time error.

Error: can't use Class as generic type argument yet, use a more specific type

How to fix this issue?


In fact, the issue I want to solve is, current Object#methods don’t always print all methods.

e.g.

# i save above playground to meta.cr
require "meta"

json = File.open("1.json") do |file|
  pp! file.methods
end

# file.methods # => [def initialize(path : ::String, fd, blocking, encoding, invalid),
#  def path() : String,
#  def size() : Int64,
#  def truncate(size = 0) : Nil,
#  def read_at(offset, bytesize),
#  def inspect(io : IO) : Nil,
#  def chown(uid : Int = -1, gid : Int = -1) : Nil,
#  def chmod(permissions : Int | Permissions) : Nil,
#  def utime(atime : Time, mtime : Time) : Nil,
#  def touch(time : Time = Time.utc) : Nil,
#  def delete() : Nil]

It only print the methods defined in File class, but methods defined in IO class not get print.

so, my plan is, print all methods defined in all superclasses, the expected behavior should similar to ruby gems looksee, current, my concerns is:

  1. i expect return a array of Class, like: [IO::FileDescriptor, IO, Reference, Object] instead of Array of String, [“IO::FileDescriptor”, “IO”, “Reference”, “Object”], this is my current issue.

  2. superclasses not print the included modules, how to get that?

Thank you.

Instead of returning Class you should return Crystal::Meta::Type.

What you means is this?

def superclasses : Array(Crystal::Meta::AbstractType)
    a = [] of Crystal::Meta::AbstractType
    {% begin %}
      {% superklass = @type.superclass %}

      until {{ superklass }} == Object
        a << Crystal::Meta::Type.new({{superklass.name(generic_args: false)}})
        {% superklass = superklass.superclass %}
      end
    {% end %}

    a << Crystal::Meta::Type.new(Object)
  end

although this code not work because dead loop.

BUG: {{ @type.name.stringify }} (Crystal::MacroExpression) at /home/zw963/Crystal/share/crystal/src/class.cr:149:11 should have been expanded (Exception)

I thought i don’t really understand why wrap those class into a Crystal::Meta::Type or Crystal::Meta::AbstractType is necessary, this may take some time to learn, but anyway, i add a Object#all_methods, it works as my expected.

def all_methods : Hash(String, Array(Crystal::Meta::AbstractMethod))
    a = {self.class.to_s => methods}

    superklass = self.superclass

    until superklass == Object
      a[superklass.to_s] = superklass.methods

      superklass = superklass.superclass
    end

    a["Object"] = Object.methods

    a
  end
file.all_methods # => {"File" =>
  [def initialize(path : ::String, fd, blocking, encoding, invalid),
   def path() : String,
   def size() : Int64,
   def truncate(size = 0) : Nil,
   def read_at(offset, bytesize),
   def inspect(io : IO) : Nil,
   def chown(uid : Int = -1, gid : Int = -1) : Nil,
   def chmod(permissions : Int | Permissions) : Nil,
   def utime(atime : Time, mtime : Time) : Nil,
   def touch(time : Time = Time.utc) : Nil,
   def delete() : Nil],
 "IO::FileDescriptor" =>
  [def noecho(),
   def noecho!(),
   def cooked(),
   def cooked!() : Nil,
   def raw(),
   def raw!(),
   def preserving_tc_mode(msg),
   def fd() : Int,
   def initialize(fd, blocking),
   def blocking(),
   def blocking=(value),
   def close_on_exec?() : Bool,
   def close_on_exec=(value : Bool),
   def fcntl(cmd, arg = 0),
   def info(),
   def seek(offset, whence : Seek = Seek::Set),
   def seek(offset, whence : Seek = Seek::Set),
   def unbuffered_pos() : Int64,
   def pos=(value),
   def fsync(flush_metadata = true) : Nil,
   def flock_shared(blocking = true),
   def flock_shared(blocking = true) : Nil,
   def flock_exclusive(blocking = true),
   def flock_exclusive(blocking = true) : Nil,
   def flock_unlock() : Nil,
   def finalize(),
   def closed?() : Bool,
   def tty?() : Bool,
   def reopen(other : IO::FileDescriptor),
   def inspect(io : IO) : Nil,
   def pretty_print(pp),
   def unbuffered_rewind(),
   def unbuffered_close(),
   def unbuffered_flush()],
 "IO" =>
  [def read(slice : Bytes),
   def write(slice : Bytes) : Nil,
   def close(),
   def closed?() : Bool,
   def check_open(),
   def flush(),
   def <<(obj) : self,
   def print(obj : _) : Nil,
   def print(*) : Nil,
   def puts(string : String) : Nil,
   def puts(obj : _) : Nil,
   def puts() : Nil,
   def puts(*) : Nil,
   def printf(format_string, args : Array | Tuple) : Nil,
   def printf(format_string, *) : Nil,
   def read_byte() : UInt8 | ::Nil,
   def read_char() : Char | ::Nil,
   def read_char_with_bytesize(peek),
   def peek_or_read_utf8(peek, index),
   def peek_or_read_utf8_masked(peek, index),
   def read_utf8_byte() : UInt8 | ::Nil,
   def read_utf8(slice : Bytes),
   def read_string(bytesize : Int) : String,
   def peek() : Bytes | ::Nil,
   def write_string(slice : Bytes) : Nil,
   def write_utf8(slice : Bytes) : Nil,
   def encoder(),
   def decoder(),
   def read_fully(slice : Bytes) : Int32,
   def read_fully?(slice : Bytes) : Int32 | ::Nil,
   def gets_to_end() : String,
   def getb_to_end() : Bytes,
   def gets(limit : Int, chomp) : String | ::Nil,
   def gets(delimiter : Char, limit : Int, chomp) : String | ::Nil,
   def gets(delimiter : Char, chomp) : String | ::Nil,
   def gets(delimiter : String, chomp) : String | ::Nil,
   def gets(chomp = true) : String | ::Nil,
   def gets_peek(delimiter, limit, chomp, peek),
   def gets_slow(delimiter : Char, limit, chomp),
   def read_line(*) : String,
   def skip(bytes_count : Int) : Nil,
   def skip_to_end() : Nil,
   def write_byte(byte : UInt8) : Nil,
   def write_bytes(object, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) : Nil,
   def read_bytes(type, format : IO::ByteFormat = IO::ByteFormat::SystemEndian),
   def tty?() : Bool,
   def each_line(*) : Nil,
   def each_line(*),
   def each_char() : Nil,
   def each_char(),
   def each_byte() : Nil,
   def each_byte(),
   def rewind(),
   def set_encoding(encoding : String, invalid : Symbol | ::Nil) : Nil,
   def encoding() : String,
   def utf8_encoding?(encoding : String, invalid : Symbol | ::Nil) : Bool,
   def has_non_utf8_encoding?() : Bool,
   def seek(offset, whence : Seek = Seek::Set),
   def pos(),
   def pos=(value),
   def tell(),
   def read_at(offset, bytesize)],
 "Reference" =>
  [def object_id() : UInt64,
   def ==(other : self),
   def ==(other : JSON::Any),
   def ==(other),
   def same?(other : Reference) : Bool,
   def same?(other : Nil),
   def dup(),
   def hash(hasher),
   def inspect(io : IO) : Nil,
   def pretty_print(pp) : Nil,
   def to_s(io : IO) : Nil,
   def exec_recursive(method),
   def exec_recursive_clone(),
   def initialize()],
 "Object" =>
  [def ==(other),
   def !=(other),
   def !~(other),
   def ===(other : JSON::Any),
   def ===(other),
   def =~(other),
   def hash(hasher),
   def hash(),
   def to_s(io : IO) : Nil,
   def to_s() : String,
   def inspect(io : IO) : Nil,
   def inspect() : String,
   def pretty_print(pp : PrettyPrint) : Nil,
   def pretty_inspect(width = 79, newline = "\n", indent = 0) : String,
   def tap(),
   def try(),
   def in?(collection : Object) : Bool,
   def in?(*) : Bool,
   def not_nil!(),
   def itself(),
   def dup(),
   def unsafe_as(type : T.class),
   def class(),
   def crystal_type_id() : Int32,
   def methods(),
   def all_methods() : Hash(String, Array(Crystal::Meta::AbstractMethod)),
   def instance_vars() : Array(String),
   def superclass() : Class,
   def superclasses() : Array(String),
   def to_json(io : IO) : Nil,
   def to_json() : String,
   def to_pretty_json(indent : String = "  ") : String,
   def to_pretty_json(io : IO, indent : String = "  ") : Nil]}

Exactly because the language doesn’t allow to create an array of Class. It’s just a workaround.

1 Like

Hi, @asterite , i use that snippet in play.crystal-lang heavily recent days, it help a lot.

In fact, i add more methods, e.g. Class#includers, Class#constants, Class#ancestors, Object#instance_vars etc.

instead waiting merge those mode into crystal, i consider create a new shard for that when stable enough, though, i am not understood all code of them, what do you think?

I consider this shard will improve a lot the experience with Crystal.

One more question, i found a issue, please check following snippet.

https://play.crystal-lang.org/#/r/dszp

Any thoughts?

+1 re introspective code

I use the hack come from @asterite play code for a while, it HELP ME A LOT. it basically replace the looksee of Ruby.

I really do consider this good things can be available for all crystalist, if merge into Crystal is impossible for now, create as a experimental shard is a good idea for people use it.

Is there really very few people like me love the tools like this or looksee for ruby?