Page 1 of 1

Cues

PostPosted: Mon Nov 17, 2008 11:54 pm
by Charles
Language systems generally have special hooks at the object level for various purposes such as comparisons, hash codes, enumerations, etc. Python does these through "quadruple underscore" methods such as:

class Customer

def __init__(self, name):
self.name = name

def __del__(self):
pass

def __cmp__(self, other):
return cmp(self.name, other.name)

def __hash__(self):
return hash(self.name)

C# does these through constructors, destructors and well-defined methods of System.Object:
class Customer {

string _name;

public Customer(string name) {
_name = name;
}

~Customer() { // destructor
}

public string Name {
get { return _name; }
}

public override bool Equals(object other) {
return Name == ((Customer)other).Name;
}

public override int GetHashCode() {
return Name.GetHashCode();
}

}

Visual Basic (VB) does the same thing except that destructors are done via a "Finalize" method (which is what C# generates anyway). And then there is Java which is similar to C#, but uses different method names such as "hashCode" vs. "getHashCode".

Cobra largely follows VB and C# except that constructors are done through "def init" which has the unfortunate consequence of conflicting with some third party libraries which has lead to some hacks in the compiler. Even worse, the current approach cannot be extended to other backends such as JVM, Parrot and Objective-C as they won't use the same method names as .NET.

How can we solve this problem? By introducing cues.

From the dictionary:

cue: a signal for action

cue: a thing said or done that serves as a signal to an actor or other performer to enter or to begin their speech or performance.


class Customer

cue init(name as String)
base.init
_name = name

cue finalize
pass

get name from var as String

cue equals(other) as bool
if this is other, return true
if other inherits Customer
return .name == other.name
else
return false

cue hash as int
return .name.hash

These cues will translate to the correct code on whatever platform you're on (.NET, JVM, etc.). However, if you still wish to write code that is oriented towards your VM, you may still do so. For example, a .NET-centric version of Customer would be:
class Customer

cue init(name as String)
_name = name

def finalize is override
base.finalize

get name from var as String

def equals(other as Object?) as bool is override
if this is other, return true
if other inherits Customer
return .name == other.name
else
return false

def getHashCode as int is override
return .name.getHashCode

Note that "cue init" is still used, but otherwise the normal .NET methods are being overridden where appropriate.


More about cues:

The word "cue" provides a good definition ("a signal for action"). It also carves out a pseudo-namespace separate from methods thereby allowing additional cues to be added in the future. Also the three letter word fits in well with other declarative words such as "def", "var" and "get".

Here are the currently planned cues:

cue init
cue finalize
cue hash as int
cue compare(other) as int
cue equals(other) as int
cue enumerate as T*

Cues are special, so there are some special rules about them, both in general and also specific to particular cues:

-- Cue declarations are exempt from needing "is override" on them.

-- Cues cannot necessarily be reflected on by name at run-time as their run-time names may be differ by platform (.NET "GetHashCode" vs. JVM "hashCode").

-- Most cues cannot be directly invoked external to the cue. You cannot say "obj.init" or "obj.enumerate" unless you declared those methods or those are coincidentally the Object level method names correpsonding to the cues. However, this is not an issue as you don't even want to invoke a cue directly. For example, "a == b" is the canonical way to compare objects for equality, and "for a in collection" is the canonical way to enumerate through the elements of a collection.

-- One exception to "do not invoke cues directly" is that *within* the declaration of a cue, you can invoke it for purposes of implementation (e.g., base.init, .init, .thing.hash).

-- The .hash cue is also an exception. It is considered "externally visible" and can be invoked like an ordinary method. (I told you cues were special.)

-- A cue alters and adds to your class as needed to implement the cue. For example the .enumerate cue when used on .NET will make your class implement IEnumerable and IEnumerable<of> while adding a method for each, for a total of two new methods. But rather than worry about the details, just remember that you can now use your object in a for loop ("for a in mycollection, ...") and pass it where enumerables are expected ("List<of T>(...)").

-- .init requires that you invoke "base.init" or ".init" immediately.

-- "def init" will continue to work for now, but at some point will generate warnings and eventually be unsupported. But fear not. The -correct-case option will be changed to -correct and take multiple arguments such as, cobra -correct:case,init ...

-- Operator overloads may be done through cues. I haven't decided yet.

I especially like the enumerate cue:

Old way:
class FileCabinet
implements IEnumerable<of Record>

var _records = List<of Record>()

def newRecord(name as String) as Record
r = Record(_records.count+1, name)
_records.add(r)
return r

def getEnumerator as IEnumerator<of Record>
return _records.getEnumerator

def getEnumerator as System.Collections.IEnumerator
implements System.Collections.IEnumerable
return .getEnumerator

New way:
class FileCabinet

var _records = List<of Record>()

def newRecord(name as String) as Record
r = Record(_records.count+1, name)
_records.add(r)
return r

cue enumerate as Record*
return _records.enumerate

Again, with the exception of "def init", you'll still be able to do things the old/.NET way. But you'll find cues portable between platforms and in some cases like .enumerate, easier to use.

None of this is implemented yet. Get your feedback in, good or bad, while you can.

Re: Cues

PostPosted: Mon Nov 24, 2008 5:55 pm
by gauthier
that seems 100% wise and elegant to me according the multitargeting plan.

Re: Cues

PostPosted: Mon Nov 24, 2008 6:01 pm
by Charles
Well I'm glad there's finally some feedback. :-)

I've been working on streams a la Comega so that you can say "int*" instead of "IEnumerable<of int>". Not only is "int*" more succinct, it's more platform neutral and fits in with the .enumerate cue.

Re: Cues

PostPosted: Mon Nov 24, 2008 7:00 pm
by gauthier
cue equals(other) as int


I just missed the equals cue, isn't bool meant to be returned or are you looking farther than C#/java?

int* provide some C root attitude here :)

will .enumerate as T* implictely implement IEnumerable<T> (well, cues aim to do just that) or it has to be specified separately?

Re: Cues

PostPosted: Tue Nov 25, 2008 1:45 am
by Charles
Woops, that was a typo. cue equals returns bool. It's cue compare that returns int.

No, the "int*" is really more like the regex syntax where * means "zero or more". Also, the db syntax where "1..*" means "one to many". And the Comega syntax where "int*" means "zero or more". It's not really related to C pointers.

Yes, cue enumerate will implicitly implement IEnumerable<T> (and the equivalent on JVM).

Re: Cues

PostPosted: Tue Dec 09, 2008 5:47 am
by jonathandavid
Note: I've edited the post to add the new terse syntax and a few more things. --Manuel


Chuck: Congratulations for this cues idea, it's great.

I think operator overlading could be implemented really smoothly using cues (and a little bit of intelligence on the compiler's part). A possible design, illustrated with a sketchy Rational class:

Code: Select all
class Rational

    get num from var as int    # In real life num and den should not be modifiable directly, as that might lead to a Rational that is not canonical
   get den from var as int
   
   cue init(n as int,d as int)
      _num = n
      _den = d
      .makeCanonical()  # GCD stuff, not implemented here for simplicity


    cue +=(other) # param. is of same class by default
      .init(other.den * .num + other.num * .den ,  .den * other.den)         # might return "this" by default,
                                                            # so that we can write a = (b += 3).
                                                            # This might not make sense to you (in C++ it's possible)
                                                            # Generates IL code for:
                                                            #   def operator += ( other as Rational) as Rational
                                                            #      .init( blah blah)
                                                            #      return this
                                                            
   
   cue -()
      return Rational(-.num, .den)
   
   cue *=(other)
      .init(.num * other.num, .den / other.den )   

   cue /=(other)
      .init(.num*other.den , .den * other.num)
      
   cue + from +=            # implictly assumes +(other) as there is no ambiguity there (unary + cannot be overloaded)
                        # generates IL code for:
                        #   def operator + ( other as Rational) as Rational
                        #      res = Rational(.num,.dem) # Calling .clone() might make more sense
                        #      return res += res
   
   cue -= from +=,-()          # implictly assumes -=(other) as there is no ambiguity there. Must specifiy -() though
                        # generates IL code for:
                        #   def operator -= ( other as Rational) as Rational
                        #      this += -other
                        #      return this
   
   cue -(other) from -=
   cue * from *=
   cue / from /=
   



For the sake of terseness, the "other" parameter in binary operators could be optional, and only mandatory in those that have both versions (like - ):

Code: Select all
class Rational

   #  ... like in the first code fragment ...


   cue +=  # "other" param by default, no ambiguity here
      .init(other.den * .num + other.num * .den ,  .den * other.den)   # Generates IL code for:      
                                                      #   def operator += ( other as Rational) as Rational      
                                                      #      .init( blah blah)      
                                                      #      return this      
                                                            
   cue -()   #empty parentheses mandatory: unary version
      return Rational(-.num, .den)
   
   #  ... other ops ...
      
   cue -(other) from -=   # parameter mandatory to distinguish from unary



Another possibility, which is IMO even more readable:

Code: Select all

class Rational

   #  ... properties, constructor like in first code fragment ...


    cue +=  # "other" param by default, no ambiguity here
      .init(other.den * .num + other.num * .den ,  .den * other.den)   # Generates IL code for:      
                                                      #   def operator += ( other as Rational) as Rational      
                                                      #      .init( blah blah)      
                                                      #      return this      
                                                            
   cue unary -         # No need for parentheses. Unary here is mandatory because of binary -
      return Rational(-num, den)
   
   cue binary /=      # Binary here is redundant, as there is no ambiguity. "other" parameter  generated by default
      .init(.num*other.den , .den * other.num)
   
   #  ... other ops ...
      
   cue binary - from -=   # "binary" mandatory to distinguish from unary

   # If we deliberately refuse to reuse operator -=:
   cue binary -         # binary mandatory to distinguish from unary; "other" parameter gererated by default
      res = Rational(this)
      res -= other
      return res



Compare this with the equivalent code in C++, and you will have C++ coders switching to Cobra by dozens ;)

The way I see this, it should flexible enough so that the user can do it the other way around if she wants:

Code: Select all
   
class Rational

   # blah blah (same as above)
      
   cue +        # param. "other" by default
      return Rational(other.den * .num + other.num * .den ,  .den * other.den)
      
   cue += from +       # Generates IL code for:
                  #   def operator += ( other as Rational) as Rational
                  #      this = this + other (I'm sure this is not possible in Cobra, what's the way to do it? Using .clone()?
                  #      return this



In this last example, the compiler might even be smart enough to emit a warning telling the programmer that the other way (defining + in terms of +=) would be more efficient.

One thing I don't see clear is how to deal with mixed arithmetic (e.g., adding a Rational to an int). C++ supports this thanks to implicit conversions, but that doesn't seem like the Cobra style. What to do then? Maybe we could add a cue telling the compiler how to convert from a Rational to and int. The compiler would use this cue whenever a mixed (int,Rational) operation is attempted:

Code: Select all
class Rational

   # blah blah (same as above)
      
   cue convert from value as int   
      return Rational(value,1)
      
   # Possible short-hand syntax: parameter is named "value" by default
   cue convert from int   
      return Rational(value,1)

   # Possible even shorter-hand syntax: convert is not really necessary
   cue from int   
      return Rational(value,1)

   # Since all finite-precision reals can be converted to Rational, the following might make sense as well:
   cue from float
      return .floatToRational(value) # implemented else where, computes canonical num,den such that num/den = value
   
      
   test
      r1 = Rational(1,2)
      r2 = r1 + 3       # Works, compiler uses "from int" cue to convert 3 int a Rational,
                          # and then perform the addition using the addition cue
      
      r3 = 3.0f + r2 # Works too, uses "from float" cue

      r2 += 4       # uses "from int" cue
      r3 -= r1
      r1 *= 2.0f    # uses "from float" cue
      r3 = -r1
      r2 = r1 / 3  # uses "from int" cue
      r3 = r2 * r1

      doSomethingWithRational(r2) # Ok
      doSomethingWithRational(4.0f) # Shall we support this? It would convert the decimal to Rational using the "from float" cue
      


As you see in the last two lins the "from T" cue might be used by the compiler to allow T parameters everywhere a Rational is expected, although this might lead to C++ style pitfalls so it should be done with care. Most of the C++ problems come from allowing the other way implicit conversion as well (i.e., from Rational to T), which tends to cause ambiguity quite often. So I think Cobra would stay safe from such problems if "to T" conversions are not allowed (they are not so useful anyway, so there's little to lose here).

It's a pity that Java has no operator overloading, but maybe Cobra could take care of the gritty details and programmers could use operator overloading in Cobra even if they use the Java backend (I guess internally it would convert things like a + b in to ugly monsters like a._add_cue(b) or something like that). Scala has operator overloading and runs on top of the JVM....

I don't know if all this "wishful thinking" is actually implementable, or even a very good idea. Having worked with painful c++ operator overloading for a while, I would really love to see a language which makes operator overaloading so simple.

Don't worry if you have different plans for operator overloading, or if you think my ideas are utter non-sense. I won't get mad like that "closures are not anonymous methods" guy ;)

Regards,
Manuel

Re: Cues

PostPosted: Tue Dec 09, 2008 11:39 am
by Charles
I don't have time just now to digest all of your post, but some quick notes after skimming it:

-- I think Cobra should allow implicit conversions. C# already has this and I think it's a useful feature. I just hadn't made a ticket for this before. ticket:90

-- Yes, you can have operator overloading in your JVM compiler. We will want to check if there is any pseudo standard for method naming conventions so that we can cooperate with other languages.

Re: Cues

PostPosted: Thu Dec 11, 2008 8:47 pm
by Charles
I think I'm changing my mind about this:

-- Cue declarations are exempt from needing "is override" on them.

While this would make the code more succinct, requiring "is override" on cues is good for the same reason it's good on methods. It shows that the developer knows what he's doing.

Note that you don't need "is override" if you have a "base.foo" invocation since that accomplishes the same thing.