Skip to content

Instantly share code, notes, and snippets.

@Timbus
Last active August 5, 2018 00:54
Show Gist options
  • Save Timbus/51d54cc7a66e69f36f8263a5d01032cf to your computer and use it in GitHub Desktop.
Save Timbus/51d54cc7a66e69f36f8263a5d01032cf to your computer and use it in GitHub Desktop.
Slightly simpler crystal extensions

Proposal for crystal-lang extension methods

This is a simplified version of my previous proposal, here: https://gist.github.com/Timbus/1f278441e7ecffbb67b664a49e18adcb

Rationale for this feature

With this document I propose two new features that work together to provide lexically scoped "extension methods" to crystal objects, with the intent of providing a safer, cleaner way to both create and use libraries.

I define "extension methods" as methods that don’t (typically) modify the state of the object they are attached to, nor really make use of OO. Some good examples of existing stdlib methods that should be extension methods include Int.times, the ‘`.to_{x}`’ methods, Enumerable.map and .each.. Basically, anything that would work as a function in a different language.

My reason for desiring a way to safely provide these methods (ie: no monkey patching), is so that libraries can provide (and use) richer, cleaner APIs without fear of stomping on other libs and breaking end-user’s code. Examples of some possible extensions are things like list operations (zipwith, cross), type conversion helpers (.to(AnotherType)), and quality-of-life methods (transform_keys, select_random, rspec’s old, better looking should method)

I honestly want Crystal to flourish, and for this I believe third party libraries need to be able to offer the same rich features and functionality that the standard library provides.

'Using' in a nutshell

  • Import a group of functions (Module methods) into lexical scope using a keyword (I propose using or import)

  • Imports are AST-tree/file scoped

  • Subsequent imports of the same name overwrite/combine with previous ones

  • Imported methods can only be called in method format, passing in the invocant as the first argument.

Full example:

module RichInt
  def self.multiply(first_thing : Int, second_thing : Int)
    first_thing * second_thing
  end
end

using RichInt
4.multiply(2)
[1,2].map &.multiply 4

Astute individuals will recognize that this is exactly how C# extension methods work.

Stretch goals

As useful as this is, I feel there should are two additional 'extra' features to better accommodate for users:

  1. Controlling imports. This gives control to the library user and resolves any clashes.

    • Import individual methods, instead of entire Modules: using ExtraModule.multiply

    • Imports can be renamed: using Asdf.bad_name as better_name

    • Imports can be unimported (unuse?): ignore Asdf.better_name, ignore Asdf

  2. Allowing default imports. I think this is super important for ease-of-use, otherwise people will be less likely to use it. It should also be the way the standard library applies its own extension methods.

    • Libraries can use a macro hook to import defaults into their caller, when loaded with require.

    • A second arg to require to suppress this behavior seems like a fair way to control this.

    • This also allows for other cool stuff (like importing private types)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment