-
-
Save keithschacht/67d4c0bd523bff067ca20140b8172cbf to your computer and use it in GitHub Desktop.
This code creates a new declaration, "df", to be used instead of "def" for those who want their Ruby methods to be typed. "df" supports passing in type declarations as part of the method declaration. I called this experiment Gelato since it's an improvement on Ruby's Sorbet. To run this example: (1) save all three files into a directory, (2) ens…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # typed: true | |
| require 'sorbet-runtime' | |
| require_relative 'gelato' | |
| extend T::Sig | |
| # Include "gelato" for a better syntax than Sorbet. Gelato is merely a DSL which writes Sorbet type definitions. | |
| # | |
| # Instead of the Sorbet two-line syntax: | |
| # | |
| # sig { params(name: T.untyped, nickname: String, member: T::Boolean, age: Integer, gender: T.untyped).returns(Integer) } | |
| # def greet(name, nickname, member, age:, gender:) | |
| # | |
| # You can write this syntax: | |
| df :greet, :name, nickname: [String], member: [T::Boolean], age: Integer, gender: T.untyped, returns: Integer do | |
| puts "Hello, #{name}! Your nickname: #{nickname}, Member? #{member}, Age: #{age}, Gender: #{gender}" | |
| name.length | |
| end | |
| result = greet("Samantha", "Sam", true, age: 30, gender: "female") | |
| # Try passing invalid types to this greet() method and you'll see Sorbet runtime checks executing. | |
| # To get static type checking working, save the "srb" file (3rd file in gist), make it executable, and run it ./srb | |
| puts "Name length: #{result}" | |
| # NOTES: | |
| # | |
| # 1) The first three arguments are positional and the last three arguments are named. :name was obviously positional, | |
| # but nickname indicates it's positional with the brackets around [String]. Contrasted this with age: Integer | |
| # | |
| # The first positional argument, :name, is untyped but only named arguments require explicitly declaring that. This | |
| # is so simple methods can stay simple. e.g. An typed method with a single argument is: | |
| # | |
| # df :expired?, :age do | |
| # 2) Gelato needs to be extended in order to support default values for arguments and to support compound values | |
| # This was my syntax idea for support default values: | |
| # | |
| # df :greet, :name, nickname: [String: "nick"], member: [T::Boolean], paid: [false], age: Integer, gender: "male", region: { String: "TX" }, country: String | |
| # | |
| # The first 4 arguments are positional (demonstrating w/ and w/o types and default values) | |
| # The last 4 arguments are named (demonstrating w/ and w/o types and default values) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # typed: false | |
| require 'sorbet-runtime' | |
| extend T::Sig | |
| def df(method_name, *args, **kwargs, &block) | |
| method_definition = df_method_definition(method_name, *args, **kwargs, &block) | |
| singleton_class.class_eval(method_definition) | |
| end | |
| def df_method_definition(method_name, *args, **kwargs, &block) | |
| sig_line = df_sig_line(method_name, *args, **kwargs) | |
| def_line = df_def_line(method_name, *args, **kwargs) | |
| return_type = kwargs.delete(:returns) || nil | |
| positional_params = df_positional_params(*args) + kwargs.select { |k,v| v.is_a?(Array) }.map { |k,v| [ k, v.first ] } | |
| named_params = kwargs.select { |k,v| !v.is_a?(Array) }.to_a | |
| all_param_names = (positional_params + named_params).map(&:first) | |
| singleton_class.instance_variable_set("@block_for_#{method_name}", block) | |
| method_definition = <<-RUBY | |
| extend T::Sig | |
| #{sig_line} | |
| #{def_line} | |
| context = Struct.new(#{all_param_names.map { |name| ":#{name}" }.join(', ')}).new(#{all_param_names.join(', ')}) | |
| block = self.singleton_class.instance_variable_get("@block_for_#{method_name}") | |
| context.instance_exec(&block) | |
| end | |
| RUBY | |
| end | |
| def df_positional_params(*args) | |
| raise "All positional arguments should be symbols but non-symbols were provided" if args.select { |a| !a.is_a?(Symbol) }.any? | |
| positional_params = args.map do |arg| | |
| [ arg, T.untyped ] | |
| end | |
| end | |
| def df_sig_line(method_name, *args, **kwargs) | |
| return_type = kwargs.delete(:returns) || nil | |
| positional_params = df_positional_params(*args) + kwargs.select { |k,v| v.is_a?(Array) }.map { |k,v| [ k, v.first ] } | |
| named_params = kwargs.select { |k,v| !v.is_a?(Array) }.to_a | |
| sig_params = (positional_params + named_params).map do |arg| | |
| "#{arg.first}: #{arg.last.name}" | |
| end.join(', ') | |
| "sig { params(#{sig_params}).#{return_type ? "returns(#{return_type})" : "void"} }" | |
| end | |
| def df_def_line(method_name, *args, **kwargs) | |
| return_type = kwargs.delete(:returns) || nil | |
| positional_params = df_positional_params(*args) + kwargs.select { |k,v| v.is_a?(Array) }.map { |k,v| [ k, v.first ] } | |
| named_params = kwargs.select { |k,v| !v.is_a?(Array) }.to_a | |
| method_params = (positional_params.map do |arg| | |
| "#{arg.first}" | |
| end + named_params.map do |arg| | |
| "#{arg.first}:" | |
| end).join(', ') | |
| "def #{method_name}(#{method_params})" | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby | |
| require 'fileutils' | |
| require_relative 'gelato' | |
| # Define the directory to scan and backup | |
| directory = '.' | |
| backup_directory = './tmp/backup' | |
| # Create the backup directory if it doesn't exist | |
| FileUtils.mkdir_p(backup_directory) | |
| # Define a method to modify the file content | |
| def modify_file_content(file_path) | |
| content = File.read(file_path) | |
| # Perform your modifications here | |
| modified_content = content.gsub(/df\s+(.*?)\s*(do\s*$|\{$)/) do |match| | |
| eval("df_sig_line(#{$1})") + "\n" + | |
| eval("df_def_line(#{$1})") | |
| end | |
| # modified_content = content | |
| File.write(file_path, modified_content) | |
| end | |
| # Backup and modify files | |
| Dir.glob("#{directory}/**/*").each do |file| | |
| next unless File.file?(file) # Skip directories | |
| backup_file = file.sub(directory, backup_directory) | |
| FileUtils.mkdir_p(File.dirname(backup_file)) | |
| FileUtils.cp(file, backup_file) | |
| modify_file_content(file) | |
| end | |
| # Run Sorbet | |
| system('srb tc') | |
| # Restore original files | |
| Dir.glob("#{backup_directory}/**/*").each do |backup_file| | |
| next unless File.file?(backup_file) # Skip directories | |
| original_file = backup_file.sub(backup_directory, directory) | |
| FileUtils.cp(backup_file, original_file) | |
| end | |
| # Optionally, clean up the backup directory | |
| FileUtils.rm_rf(backup_directory) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment