Skip to content
This repository has been archived by the owner on Mar 1, 2023. It is now read-only.

Commit

Permalink
Merge pull request #14 from braiden-vasco/prepare_for_release
Browse files Browse the repository at this point in the history
Prepare for release
  • Loading branch information
Braiden Vasco committed Jan 18, 2016
2 parents b79ae8b + fffc2b7 commit c305a9e
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 155 deletions.
226 changes: 137 additions & 89 deletions README.md
Expand Up @@ -8,138 +8,186 @@ Typeclass

Haskell type classes in Ruby.

Examples
--------
Summary
-------

File [examples/animals.rb](examples/animals.rb):
Current state:

```ruby
# This comes from Rust traits example
# http://rustbyexample.com/trait.html

require 'typeclass'
* Syntactic identity with Haskell type classes

Animal = Typeclass.new a: Object do
fn :name, [:a]
fn :noise, [:a]
Goals:

fn :talk, [:a] do |a|
"#{name a} says \"#{noise a}\""
end
end
* Static type checking
* Strong optimization

Dog = Struct.new(:name)
Usage
-----

Typeclass.instance Animal, a: Dog do
def name(a)
a.name
end
**The gem is under development. Don't try to use it in production code.**

def noise(_a)
'woof woof!'
end
end
To install type in terminal

Sheep = Struct.new(:name)
```sh
gem install typeclass
```

Typeclass.instance Animal, a: Sheep do
def name(a)
a.name
end
or add to your `Gemfile`

def noise(_a)
'baaah'
end
end
```ruby
gem 'typeclass', '~> 0.1.1'
```

include Animal
To learn how to use the gem look at the [examples](/examples/).

dog = Dog['Spike']
sheep = Sheep['Dolly']
Concept
-------

fail unless talk(dog) == 'Spike says "woof woof!"'
fail unless talk(sheep) == 'Dolly says "baaah"'
```
The main goals of this project is to create statically typed subset of Ruby
inside dynamically typed Ruby programs as a set of functions which know
types of it's arguments. There is something like function decorator
which checks if function is correctly typed after it is defined.
Type declarations are needed for typeclass definition only. All other types
are known due to type inference, so the code looks like normal Ruby code.

-----
Of course there is a runtime overhead due to the use of type classes.
Therefore another important goal is an optimiaztion which is possible
because of the known types. It can be performed with bytecode generation
at runtime. In this way the bytecode generated by Ruby interpreter
will be replaced with the optimized code generated directly from the source.
If the optimized bytecode can not be generated due to some reasons
(no back end for the virtual machine, for example), the code can be
interpreted in the usual way because it is still a normal Ruby code.

File [examples/eq.rb](examples/eq.rb):
Example
-------

```ruby
require 'typeclass'
Please read [this article](https://www.haskell.org/tutorial/classes.html)
if you are unfamiliar with Haskell type classes (understanding of Rust
traits should be enough).

# Class Data.Eq from Haskell standard library
# https://hackage.haskell.org/package/base-4.8.1.0/docs/Data-Eq.html
Let's look at the following example and realize which parts of the code
can be statically typed.

Eq = Typeclass.new a: Object do
fn :equal, [:a, :a] do |a1, a2|
!noteq(a1, a2)
end

fn :noteq, [:a, :a] do |a1, a2|
!equal(a1, a2)
end
```ruby
Show = Typeclass.new a: Object do
fn :show, [:a]
end

# Complex number

Cmplx = Struct.new(:real, :imag) do
def self.scan(s)
m = s.match(/^(?<real>\d+(\.\d+)?)\s*\+\s*(?<imag>\d+(\.\d+)?)i$/)
Cmplx[m[:real].to_f, m[:imag].to_f]
Typeclass.instance Show, a: Integer do
def show(a)
"Integer(#{a})"
end
end

def to_s
"#{real} + #{imag}i"
Typeclass.instance Show, a: String do
def show(a)
"String(#{a.dump})"
end
end

# Two complex numbers are equal if and only if
# both their real and imaginary parts are equal.
puts Show.show(5) #=> Integer(5)
puts Show.show('Qwerty') #=> String("Qwerty")
```

Typeclass.instance Eq, a: Cmplx do
def equal(a1, a2)
a1.real == a2.real && a1.imag == a2.imag
end
end
As you can see, that there is no annoying
[typesig's](https://rubygems.org/gems/rubype),
[typecheck's](https://rubygems.org/gems/typecheck),
[sig's](https://rubygems.org/gems/sig),
and again [typesig's](https://github.com/plum-umd/rtc).
Definitions of type classes and instances, and function signatures
looks like typical Haskell code. The functions, in turn, are just
Ruby methods.

include Eq
Nevertheless, the types of the arguments are known and can be checked
in `Typeclass#instance` method after it's block is executed.

a = Cmplx[3.5, 2.7]
Optimizations
-------------

b = Cmplx.scan '3.5 + 2.7i'
c = Cmplx.scan '1.9 + 4.6i'
### Interaction between parts of the code

fail unless equal(b, a)
fail unless noteq(c, a)
```
There are a few options how the statically and dynamically typed
parts of code interact with one another.

-----
* statically typed code calls dynamically typed code
* dynamically typed code calls statically typed code
* statically typed code calls statically typed code

Let's look at each separately.

File [examples/inheritance.rb](examples/inheritance.rb):
#### Statically typed code calls dynamically typed code

```ruby
require 'typeclass'
Foo = Typeclass.new a: Object, b: Object do
fn :foo, [:a, :b]
end

Foo = Typeclass.new a: Object do
fn :foo, [:a]
class Bar
def bar(b)
# ...
end
end

Bar = Typeclass.new Foo[:a], a: Object do
fn :bar, [:a]
Typeclass.instance Foo, a: Bar, b: Integer do
def foo(a, b)
a.bar(b)
end
end
```

In this case we can not know how method `Bar#bar` uses it's arguments,
so we can only call the method without any checks and optimizations.

#### Dynamically typed code calls statically typed code

Typeclass.instance Foo, a: Integer do
def foo(a)
a * 2
```ruby
Foo = Typeclass.new s: Object do
fn :foo, [:s]
end

Typeclass.instance Foo, s: String do
def foo(s)
s + s.reverse
end
end

Typeclass.instance Bar, a: Integer do
def bar(a)
foo(a + 1)
Typeclass.instance Foo, s: Symbol do
def foo(s)
(s.to_s + s.to_s.reverse).to_sym
end
end

fail unless Bar.bar(2) == 6
Foo.foo 'abc' #=> "abccba"
Foo.foo :abc #=> :abccba
```

In the last two lines the function is called with arguments of two different
types, so we have to choose the right typeclass' instance at runtime.
This operation has a huge runtime overhead which can not be avoided.

But there is a solution. Sometimes the right instance can be definitely
determined by the type of the first argument of a function. In this case
the function can be turned into method of it's first argument.
This is called infix function, and will be described in future versions
of this document.

#### Statically typed code calls statically typed code

This is the most convenient option for optimizations. Presumably the code
will be close to the machine code in execution speed and memory consumption.

### Additional optimization possibilities

The previously described model has great ability to optimize business logic
only. This is absolutely pointless.

The gem aims to allow to effectively "crunch numbers" in Ruby, what means
strongly optimized arithmetic. The main problem is that Ruby's standard
library is written in Ruby and C, so we can not analyze it's code at runtime.

Nevertheless, it is a small problem. The Ruby's standard library is well-known.
We can assume it's properties. This should be enough for optimizations
of arithmetics (the result of `2 + 2` is evident). The Ruby's ability of
monkey-patching (when method `Integer#*` is redefined to return something other
than result of integer multiplication, for example) can be ignored because this
is a terrible practice.
27 changes: 0 additions & 27 deletions Rakefile
@@ -1,5 +1,3 @@
# frozen_string_literal: true

require 'rubygems'

gemspec = Gem::Specification.load('typeclass.gemspec')
Expand Down Expand Up @@ -35,28 +33,3 @@ GitHubChangelogGenerator::RakeTask.new do |config|
config.user = github_user
config.project = github_project
end

desc 'Render examples to README'
task :examples do
EXAMPLES_DIR = 'examples'.freeze
README = 'README.md'.freeze
SUBTITLE = 'Examples'.freeze
EXPR = "#{SUBTITLE}\n#{'-' * SUBTITLE.length}\n".freeze
REGEXP = /#{EXPR}/

examples = Dir["#{EXAMPLES_DIR}/*"].each_with_index.map do |filename, index|
example = File.read filename

<<-END
#{"-----\n\n" unless index.zero?}File [#{filename}](#{filename}):
```ruby
#{example}```
END
end.join("\n")

input = File.read README
pos = input =~ REGEXP
output = "#{input[0...pos]}#{EXPR}\n#{examples}"
File.write README, output
end
35 changes: 3 additions & 32 deletions lib/typeclass.rb
Expand Up @@ -9,10 +9,6 @@
class Typeclass < Module
include Superclass::TypeclassMixin

# @!attribute [r] superclasses
# @return [Array<Typeclass::Superclass>] Type class superclasses.
attr_reader :superclasses

# @!attribute [r] constraints
# @return [Hash] Type parameter constraints.
attr_reader :constraints
Expand All @@ -38,13 +34,13 @@ def initialize(*superclasses, constraints, &block)

Superclass.check! superclasses
Typeclass.check_constraints! constraints
Typeclass.check_superclass_args! constraints, superclasses
Superclass.check_superclass_args! constraints, superclasses

@superclasses = superclasses
@constraints = constraints
@instances = []

superclasses.map(&:typeclass).each(&method(:inherit))
superclasses.each(&method(:inherit))

instance_exec(&block)
end
Expand Down Expand Up @@ -141,7 +137,7 @@ def self.type?(object)
TYPES.any? { |type| object.is_a? type }
end

# Check is type parameter constraints have valid format.
# Check if type parameter constraints have valid format.
# Raise exceptions if format is invalid.
#
# @param constraints [Hash] Type parameter constraints.
Expand All @@ -162,12 +158,6 @@ def self.check_constraints!(constraints)
end
end

def self.check_superclass_args!(constraints, superclasses)
fail ArgumentError unless superclasses.all? do |superclass|
superclass.args.all? { |arg| constraints.key? arg }
end
end

private

# Available constraint types.
Expand All @@ -178,25 +168,6 @@ def self.check_superclass_args!(constraints, superclasses)
# @see Typeclass::Instance::Params.check_raw_params!
BASE_CLASS = Object

def check_superclasses_implemented!(raw_params)
fail NotImplementedError unless superclasses.all? do |superclass|
superclass.implemented? raw_params
end
end

def inherit(typeclass)
typeclass.singleton_methods.each do |method_name|
p = typeclass.method method_name

define_singleton_method method_name, &p
define_method method_name, &p
end

typeclass.superclasses.each do |superclass|
inherit superclass.typeclass
end
end

# Declare function signature with optional default block.
#
# @example
Expand Down

0 comments on commit c305a9e

Please sign in to comment.