Remix.run Logo
jez 3 hours ago

It's an interesting approach. From my skim, the way it works:

1. Parse the files with a Ruby parser, collect all method definition nodes

2. Using location information in the parsed AST, and the source text of the that was parsed, splice the parameters into two lambda expressions, like this[1]:

     "-> (#{method_node.parameters.slice}) {}"
3. Evaluate the first lambda. This lets you reflect on `lambda.parameters`, which will tell you the parameter names and whether they're required at runtime, not just statically

4. In the body of the second lambda, use the `lambda.parameters` of the first lambda in combination with `binding.get_local_variable(param_name)`. This allows you to get the runtime value of the statically-parsed default parameters.

This is an interesting and ambitious architecture.

I had though in the past about how you might be able to get such a syntax to work in pure Ruby, but gave up because there is no built-in reflection API to get the parameter default values—the `Method#parameters` and `UnboundMethod#parameters` methods only give you the names of the parameters and whether they are optional or required, not their default values if they are optional.

This approach, being powered by `binding` and string splicing, suffers from problems where a name like `String` might mean `::String` in one context, or `OuterClass::String` in another context. For example:

    class MyClass
      include LowType
      class String; end
      def say_hello(greeting: String); end
    end

    MyClass.new.say_hello(greeting: "hello")
This program does not raise an exception when run, despite not passing a `MyClass::String` instance to `say_hello`. The current implementation evaluates the spliced method parameters in the context of a `binding` inside its internal plumbing, not a binding tied to the definition of the `say_hello` method.

An author could correct this by fully-qualifying the constant:

    class MyClass
      include LowType
      class String; end
      def say_hello(greeting: MyClass::String); end
    end

    MyClass.new.say_hello(greeting: "hello") # => ArgumentTypeError
and you could imagine a Rubocop linter rule saying "you must use absolutely qualified constant references like `::MyClass::String` in all type annotations" to prevent a problem like this from happening if there does not end up being a way to solve it in the implementation.

Anyways, overall:

- I'm very impressed by the ingenuity of the approach

- I'm glad to see more interest in types in Ruby, both for runtime type checking and syntax explorations for type annotations

[1] https://codeberg.org/Iow/type/src/branch/main/lib/definition...

radiospiel 35 minutes ago | parent | next [-]

> how you might be able to get such a syntax to work in pure Ruby, but gave up because there is no built-in reflection API to get the parameter default values

what I have done successfully here https://github.com/radiospiel/simple-service/blob/master/lib... is to install a TracePoint which immediately throws, and then call the method. The tracepoint then receives the value of the default arguments.

Not pretty, and I wouldn't run this in production critical parts of a system, but it works.

lowtype 10 minutes ago | parent | prev [-]

[dead]