/named_value_class

Quickly add class delegate constants which output their names, not their values.

Primary LanguageRubyOtherNOASSERTION

NamedValueClass

Quickly add customizable class delegate constants which output their names, not their values. This may be desirable for some DSLs.

Please ask questions or scold me about this here.

Features

  • Creates a class for constants that delegate most work to the stable and efficient ruby core classes.

  • #to_s and #inspect output is the constant’s name and not it’s value.

  • Allows for lowercase constant names.

  • Generates helper methods to access constants based on the parameters set when creating the constants.

  • Provides a mapping from value back to constant.

  • Provides helpers for defining special math/operator rules.

  • Only dependency is the ‘delegate’ library which is already in the ruby stdlib so… really no dependancies.

Installation

gem install named_value_class

Requirements

Requires ruby 1.9. Tested with:

  • ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-darwin10.8.0]

  • jruby 1.6.4 (ruby-1.9.2-p136) (2011-08-23 17ea768) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_29) [darwin-x86_64-java]

Usage

First a complete example:

require 'named_value_class'

NamedValueClass Pitch:Fixnum, constrain:0..11 do
  minus_a :Interval do |lhs,minus,rhs|
    result = minus.call(rhs) % 12
    is_sharp? ? Pitch.sharps(result) : Pitch.flats(result)
  end

  all_operators_with_a :Pitch, raise:SyntaxError

  def as_flat
    self.class.naturals[self] || self.class.flats[self]
  end

  def as_sharp
    self.class.naturals[self] || self.class.sharps[self]
  end
end

Pitch Bs:  0,   sharp:true  
Pitch C:   0, natural:true 
Pitch Cs:  1,   sharp:true 
Pitch Db:  1,    flat:true 
Pitch D:   2, natural:true 
Pitch Ds:  3,   sharp:true 
Pitch Eb:  3,    flat:true 
Pitch E:   4, natural:true 
Pitch Fb:  4,    flat:true 
Pitch Es:  5,   sharp:true 
Pitch F:   5, natural:true 
Pitch Fs:  6,   sharp:true 
Pitch Gb:  6,    flat:true 
Pitch G:   7, natural:true 
Pitch Gs:  8,   sharp:true 
Pitch Ab:  8,    flat:true 
Pitch A:   9, natural:true 
Pitch As: 10,   sharp:true 
Pitch Bb: 10,    flat:true 
Pitch B:  11, natural:true 
Pitch Cb: 11,    flat:true

What does this do?

First create a new class named Pitch that delegates most work to Fixnum with a value between 0 and 11 inclusive; Open a block to define some specialized methods:

NamedValueClass Pitch:Fixnum, constrain:0..11 do

Define how to handle the “Pitch - Interval” use case:

minus_a :Interval do |lhs,minus,rhs|

When defining operators the blocks are always yieled: the LHS (left hand side), a proc with the original, default implementation of the operator you are redefining, and the RHS. The passed proc’s receiver is the LHS so in the following code “minus.call(rhs)” performs “lhs - rhs”.

result = minus(rhs) % 12

Since we plan on setting boolean parameters named ‘sharp’ and ‘flat’ when we define Pitch constants later we can use methods is_sharp?, is_flat?, Pitch.sharps(value) and Pitch.flats(value):

  is_sharp? ? Pitch.sharps(result) : Pitch.flats(result)
end

It doesn’t make any sense to do math with a Pitch on the left and right sides so all operators should raise a SyntaxError. Examples: Ds + C, Eb / A

all_operators_with_a :Pitch, raise:SyntaxError

Define some instance methods:

  def as_flat
    self.class.naturals[self] || self.class.flats[self]
  end

  def as_sharp
    self.class.naturals[self] || self.class.sharps[self]
  end
end

Now create Pitch constants:

Pitch Bs:  0,   sharp:true  
Pitch C:   0, natural:true 
Pitch Cs:  1,   sharp:true 
Pitch Db:  1,    flat:true 
Pitch D:   2, natural:true 
Pitch Ds:  3,   sharp:true 
Pitch Eb:  3,    flat:true 
Pitch E:   4, natural:true 
Pitch Fb:  4,    flat:true 
Pitch Es:  5,   sharp:true 
Pitch F:   5, natural:true 
Pitch Fs:  6,   sharp:true 
Pitch Gb:  6,    flat:true 
Pitch G:   7, natural:true 
Pitch Gs:  8,   sharp:true 
Pitch Ab:  8,    flat:true 
Pitch A:   9, natural:true 
Pitch As: 10,   sharp:true 
Pitch Bb: 10,    flat:true 
Pitch B:  11, natural:true 
Pitch Cb: 11,    flat:true

An irb session with the above code loaded:

~/d/m/n/examples irb
>> load "./pitch_class.rb" #=> true
>> include Pitch::NamedValues #=> Object
>> B #=> B
>> C #=> C
>> C.value #=> 0
>> Eb.value #=> 3
>> C + 2 #=> D
>> Eb - 4 #=> Bs
>> [C,Eb] #=> [C, Eb]
>> C > Eb #=> false
>> Pitch.flats #=> {1=>Db, 3=>Eb, 4=>Fb, 6=>Gb, 8=>Ab, 10=>Bb, 11=>Cb}
>> Pitch.sharps #=> {0=>Bs, 1=>Cs, 3=>Ds, 5=>Es, 6=>Fs, 8=>Gs, 10=>As}
>> C.is_sharp? #=> nil
>> C.is_flat? #=> nil
>> C.is_natural? #=> true
>> Pitch H:0, insane:true #=> H
>> H #=> H
>> C.is_insane? #=> nil
>> H.is_insane? #=> true
>> Pitch.insanes #=> {0=>H}

Thoughts

I’m still not settled on a best practice for coercion, so it remains missing. from the helpers and specs. You can of course just write a coerce method for any named value class you create.

This gem was created out of a re-factor of my music theory calculator gem, Peas. It greatly reduced the code clutter of that gem. I’d say it reduced the number lines by half at least.

Issues

  • If you run into this error: “super from singleton method that is defined to multiple classes is not supported; this will be fixed in 1.9.3 or later” you should know the ruby is producing this message, not me. Try declaring instances directly after the NamedValueClass call for each new type. I’m still trying to accurately repoduce this problem in specs and a solution evades me currently. This is crazy annoying and makes the gem far less useful at the moment. I declare that it will be solved before v1.0. Free beer for whoever can finish either of the above tasks before me.

Author

License

Apache 2.0, See the file LICENSE

Copyright © 2011 Michael Garriss