HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

parametric polymorphism and type classes

10-07-2007, 06:36 AM#1
PipeDream
Haskell has a pretty good type system. We can't be as cool as a modern FP language because we don't have garbage collection, but I think we can still do some nice things. Namely we can get good abstractions with out the OO notion of a class, and I think we can do it with out abandoning our (i.e. Vex's) work so far. There are two features in particular that I think we can rip out of haskell to our benefit. One is parametric polymorphism, which has in the been adopted by mainstream languages in the form of generics. We can use these to write generic code at very little cost. The second is type classes, which give us functionality similar to traditional OO virtual classes, but are lighter weight. They avoid the virtual indirect call. Using them effectively comes at the cost of throwing out our old methods.

Parametric Polymorphism
Collapse JASS:
struct list<a>
    a x
    list<a> xs
    static method create takes a x, list<a> xs returns list<a>
        local list<a> p = list<a>.allocate(x,xs)
        set p.x = x
        set p.xs = xs
        return p
    endmethod
endstruct

And we'd use it for something like
Collapse JASS:
local list<Int> int_list = list<Int>.create(37,0)

Now we can define generic functions such as length:
Collapse JASS:
function length<a> takes list<a> lst returns integer
    if lst == 0 then
        return 0
    else
        return 1 + length<a>(lst.xs)
    endif
endfunction

This could also be a method of list
Collapse JASS:
method length takes nothing returns integer // no <a> because the struct parameterizes it
    if this == 0 then
        return 0
    else
        return 1 + .xs.length()
    endif
endstruct

More interesting is to have some higher order functions like foldr.
Collapse JASS:
function interface istos takes integer, string returns string
function prepend takes integer x, string s returns string
    return I2S(s)+s
endfunction

function foldr<a,b> takes istos f, a accum, list<b> lst returns a
    if lst == 0 then
        return accum
    else
        return foldr<a,b>(f,f.evaluate(lst.x,accum),lst.xs)
    endif
endfunction

function bignum_to_s takes list<integer> n returns string
    return foldr<string,integer>(istos.prepend,"",n)
endfunction

Compilation for these functions is straight forward, because you can't touch the parameterized type. Just replace the types, e.g.
Collapse JASS:
function foldr_string_integer takes integer f, string accum, integer lst returns string
    if lst == 0 then
        return accum
    else
        return foldlr_string_integer(f,evaluate_string_integer(f,x[lst],accum),xs[lst])
    endif
endfunction

Type Classes
Interfaces for structs are kind of weak, and we all know how lame inheritance is. There's another way to express that we want functions that operate on families of types.

Collapse JASS:
//Interface is a fine name for it too
class Eq a
    function cmp takes a x, a y returns boolean
endclass
Now we can use this to specify that a function's generic argument must have all the operations of Eq on it.
Collapse JASS:
function notequal<Eq a> takes a x, a y returns boolean
    return not cmp<a>(x,y)
endfunction

Let's make this work for integers.
Collapse JASS:
instance Eq integer
    function cmp takes integer x, integer y returns boolean
        return x == y
    endfunction
endinstance

And let's compile
Collapse JASS:
    local boolean b = notequal<integer>(0,1)
becomes
Collapse JASS:
function cmp_integer takes integer x, integer y returns boolean
    return x == y
endfunction
function notequal_integer takes integer x, integer y returns boolean
    return cmp_integer(x,y)
endfunction
// ...
    local boolean b = notequal_integer(0,1)


Example, boxed sorted lists
Collapse JASS:
class Ord a
    function cmp takes a x, a y returns boolean
endclass

struct list<a>
    a x
    list<a> xs
    static method create takes a x, list<a> xs returns list<a>
        local list<a> p = list<a>.allocate(x,xs)
        set p.x = x
        set p.xs = xs
        return p
    endmethod
endstruct

struct SortedList<Ord a>
    list<a> x
    static method create takes nothing returns SortedList<a>
        local SortedList<a> sl = SortedList<a>.allocate()
        set sl.x = 0
        return sl
    endmethod

    method insert takes a newv returns nothing
        local list<a> cur = .x
        if cur == 0 then
            set .x = list<a>.create(newv,0)
            return
        endif

        loop
            exitwhen cur.xs == 0 or cmp<a>(newv,cur.x)
            set cur = cur.xs
        endloop
        set cur.xs = list<a>.create(newv,cur.xs)
    endmethod
endstruct

instance Ord vector
    function cmp vector x, vector y returns boolean  // strange for cmp to find its way outside of the vector class
        return x.length() > y.length()
    endfunction
endinstance

function foo takes nothing returns nothing
    local SortedList<vector> lst = SortedList<vector>.create()
    call lst.insert(vector.create(300.0,200.0))
    call lst.insert(vector.create(5.0,7.0))
    call lst.insert(vector.create(1000.0,0.0))
endfunction

And we can make sorted collections themselves a class. Here we have to accept the strangeness of having methods outside of the struct definition by abandoning methods and just using functions.
Collapse JASS:
class SortedColl (Ord a)
    function insert takes SortedColl<a>, a x returns nothing
endclass

struct SortedList<Ord a>
    list<a> x
endstruct

instance SortedColl SortedList<Ord a>
    function insert takes SortedList<a>, a x returns nothing
        ...
    endfunction
endinstance

function AddTwoToColl<SortedColl a<Ord b>> takes SortedColl<a<Ord b>> sc, b x, b y returns nothing
    call insert<SortedColl a<b>>(x)
    call insert<SortedColl a<b>>(y)
endfunction

function foo takes nothing returns nothing
    local SortedList<integer> sl = SortedList<integer>.create()
    call AddTwoToColl<SortedList<integer>>(sl,3,5)
endfunction

With out type inference it might just be useless though, typing out insert<SortedColl a<b>>(x,y) is pretty lame. The goal is to have polymorphic functions, so that you just write insert.

Summary
  • Generics let you write functions with out specifying the type of an argument, when a function does not need to touch the value of that argument.
  • Generics play nicely with the existing class system
  • We can replace interfaces and the existing classes with type classes, which specify a set of functions a type must support
  • This is lighter weight than multiple inheritance but more flexible
  • It leads to some pretty verbose types, but only for things we can't do now.
  • Anyway that might be fixable with type inference.
  • Type classes have the nice property of letting us work with native types right alongside structs.

There is an object oriented extension to haskell which might contain the solution to the method problem for type classes. I haven't looked at it.
10-07-2007, 09:50 AM#2
PitzerMike
Generics are lovely.
10-07-2007, 03:30 PM#3
Vexorian
err dunno why nobody looked at this:

http://www.wc3campaigns.net/pastebin...9bf38be3066a0f

Expand quote:

this doesn't make sense, or am I missing something? Wouldn't the whole istos function interface also need type parameters? I would also say you don't want to I2S s.

Edit:
I was planning not to use <> at all and to require some type usage to call stuff, but I guess using <> got some merit for some extra nesting, but then we end up having more confusing syntax, you are right this is the only way to have higher order functions like map, I don't know foldr but I do know map...

Collapse JASS:
function interface err<type t> takes t returns t

function map<type t> takes list<t> L, err<t> F returns list<t>
    return list<t>.create( F.evaluate(L.x) , map<t>(L.sx, F)  )
endfunction

Posting this mostly to make sure I am understanding you...
10-07-2007, 04:33 PM#4
PipeDream
Yes, I screwed up that example. That may significantly complicate things too. Let's try compiling another example.

Collapse JASS:
class Show a
    function show takes a x returns string
endclass

instance Show integer
    function show takes integer x returns string
        return I2S(x)
    endfunction
endinstance

instance Show unit
...
endinstance

instance Show list<a>
    function show takes list<a> lst returns string
        return show<a>(lst.x)+" : "+show<list<a>>(lst.xs)
    endfunction
endinstance

function foo takes nothing returns nothing
    local list<integer> lst = ...
    local list<list<unit>> lst2 = ...
    call show<list<integer>>(lst) 
    call show<list<list<unit>>>(lst2)  // not having to specify the type of show here is where it becomes really nice
endfunction

I'd implement your example like this
Collapse JASS:
function interface k1<a,b> takes a returns b

// ignore termination case for now
function map<a,b> takes k1<a,b> f, list<a> lst returns list<b>
    return list<b>.create( f.evaluate(lst.x), map<a,b>(lst.xs, f) )
endfunction

Example
Collapse JASS:
function bar takes nothing returns nothing
    local list<integer> lst = ...
    local list<string> lst2
    set lst2 = map(k1<integer,string>.show<integer>,lst)
endfunction
compiles to
Collapse JASS:
function interface k1_integer_string takes integer returns string
function map_integer_string takes k1_integer_string f, list_integer lst returns list_string
    return list_string.create(f.evaluate(lst.x),lst.xs)
endfunction
function show_integer takes integer x returns string
    return I2S(x)
endfunction
function bar takes nothing returns nothing
    local list_integer lst = ...
    local list_string lst2
    set lst2 = map_integer_string(k1_integer_string.show_integer,lst)
endfunction

Hm works out fine. In fact it's practically just a macro.
10-07-2007, 07:57 PM#5
Vexorian
should not be taken seriously:

Collapse JASS:
function show<integer> takes integer x returns string
        return I2S(x)
endfunction

function show<unit> takes unit x returns string
      return GetUnitName(x)+I2S(H2I(x))     
endfunction

function DebugMessage<showable f> takes string vname, f v returns nothing
     call BJDebugMsg( vname+" : "+show<f>(v) )
endfunction


function aaa takes unit u, integer k, real f returns nothing
     call DebugMessage<unit>("u", u)
     call DebugMessage<integer>("k", k)
     call DebugMessage<real>("f", f)
endfunction

In a perfect world, this should be enough, and the instantiation of DebugMessage<real> should be enough to popup an error message about not having show<real> declared, of course it isn't usually the case...



Collapse JASS:
//not sure, perhaps it would also work if it was like the other one
class showable
     function show<showable> takes showable x returns string
endclass
//perhaps we could use typeclass so people don't confuse it with the more evil OOP class concept...


function show<integer> takes integer x returns string
        return I2S(x)
endfunction

function show<unit> takes unit x returns string
      return GetUnitName(x)+I2S(H2I(x))     
endfunction

function DebugMessage<org f> takes string vname, f v returns nothing
     call BJDebugMsg( vname+" : "+show<f>(v) )
endfunction

function aaa takes unit u, integer k, real f returns nothing
     call DebugMessage<unit>("u", u)
     call DebugMessage<integer>("k", k)
     call DebugMessage<real>("f", f)
endfunction

This would just make the parsing easier. At the time you use DebugMessage<> anything should tell the compiler to make sure the type is implementing the required functions to be considered a type that belongs to the showable type (in this case only requires Show<> and the error would be, "show<real> is not declared and therefore real does not belong to the showable class" And also at the time of parsing DebugMessage we can popup errors when we find usage of stuff not specific to the declared showable class...
10-07-2007, 08:33 PM#6
PipeDream
I like dropping instance, but I don't like the 'doesn't need to belong to any class' keyword.
Fixing up the collection example
Collapse JASS:
// a simple list type
struct list<a>
    a x
    list<a> xs
endstruct

function cons<a> takes a x, list<a> xs returns list<a>
    local list<a> lst = list<a>.create()
    set lst.x = x
    set lst.xs = xs
    return lst
endfunction

// equality on some element for a set of unique elements
class Eq a
    function cmp a x, a y returns boolean
endclass

// instanciation of the class for integers
function cmp<integer> takes integer x, integer y returns boolean
    return x == y
endfunction

// a set class for unique elements
class Set s<Eq a>
    function intersect takes s<a>, s<a> returns s<a>
    function has takes s<a>, a returns boolean
endclass

// instance of set for lists
function has<list<Eq a>> takes list<a> lst, a ele returns boolean
    if lst == 0 then
        return false
    elseif cmp<a>(lst.x,ele) then
        return true
    else
        return has<list<a>>(lst.xs,ele)
    endif
endfunction

function intersect<list<Eq a>> takes list<a> u, list<a> v returns list<a>
    if u == 0 then
        return v
    endif
    if has<list<a>>(v,u.x) then
        return cons<a>(u.x,intersect<list<a>>(u.xs,v))
    else
        return intersect<list<a>>(u.xs,v)
    endif
endfunction

// Use
function dostufftosets<Set a> takes a u, a v returns a
    return intersect<a>(u,v)
endfunction

function foo takes nothing returns nothing
    local list<integer> lst = ...
    local list<integer> lst2 = ...
    local list<integer> lstinter = dostufftosets<list<integer>>(lst,lst2)
endfunction
Compiles to
Collapse JASS:
// first check that intersect<list<integer>> is valid
// check that intersect defined for lists
// it's defined for list<Eq a>
// check that integer is in Eq

// first pass.  Erase innermost type variables

// a simple list type
struct list_integer
    integer x
    list_integer xs
endstruct

function cons_integer takes integer x, list_integer xs returns list_integer
    local list_integer lst = list_integer.create()
    set lst.x = x
    set lst.xs = xs
    return lst
endfunction

// instanciation of the class for integers
function cmp_integer takes integer x, integer y returns boolean
    return x == y
endfunction


// a set class for unique elements
class Set list_integer // here we matched the entire s<a> with list_integer, while matching a with integer
    function intersect takes list_integer, list_integer returns list_integer
    function has takes list_integer, integer returns boolean
endclass

// instance of set for lists
function has<list_integer> takes list_integer lst, integer ele returns boolean
    if lst == 0 then
        return false
    elseif cmp_integer(lst.x,ele) then
        return true
    else
        return has<list_integer>(lst.xs,ele)
    endif
endfunction

function intersect<list_integer> takes list_integer u, list_integer v returns list_integer
    if u == 0 then
        return v
    endif
    if has<list_integer>(v,u.x) then
        return cons_integer(u.x,intersect<list_integer>(u.xs,v))
    else
        return intersect<list_integer>(u.xs,v)
    endif
endfunction

function dostufftosets<list_integer> takes list_integer u, list_integer v returns a
    return intersect<list_integer>(u,v)
endfunction

function foo takes nothing returns nothing
    local list_integer lst = ...
    local list_integer lst2 = ...
    local list_integer lstinter = dostufftosets<list_integer>(lst,lst2)
endfunction
And repeat to erase the next level, although you can probably do it all at once.
10-14-2007, 02:20 AM#7
PipeDream
Proof of concept (i.e. toy) generics compiler

In:
Collapse JASS:
struct list<a>
    a x
    list<a> xs
endstruct

function cons<a> takes a x, list<a> xs returns list<a>
    local list<a> p = list<a>.create()
    set p.x = x
    set p.xs = xs
    return p
endfunction

struct bar
    real x
    integer n
endstruct

function foo takes nothing returns nothing
    local list<integer> lst = cons<integer>(37,cons<integer>(2,0))
    local list<bar> lst2 = cons<bar>(bar.create(),0)
    set lst = foo()
endfunction

Out:
Collapse JASS:
struct list_integer
    integer x
    list_integer xs
endstruct
struct list_bar
    bar x
    list_bar xs
endstruct
function cons_integer takes integer x, list_integer xs returns list_integer
    local list_integer p = list_integer.create()
    set p.x = x
    set p.xs = xs
    return p
endfunction
function cons_bar takes bar x, list_bar xs returns list_bar
    local list_bar p = list_bar.create()
    set p.x = x
    set p.xs = xs
    return p
endfunction
struct bar
    real x
    integer n
endstruct
function foo takes nothing returns nothing
    local list_integer lst = cons_integer(37,cons_integer(2,0))
    local list_bar lst2 = cons_bar(bar.create(),0)
    set lst = foo()
endfunction

Or handle vars:
Collapse JASS:
function GetHandle<a> takes handle subject, string name returns a
    return GetStoredInteger(LocalVars(),I2S(H2I(subject)),name)
    return null
endfunction

function foo takes nothing returns nothing
    local location loc = GetHandle<location>(nu,"foo")
endfunction
becomes
Collapse JASS:
function GetHandle_location takes handle subject, string name returns location
    return GetStoredInteger(LocalVars(),I2S(H2I(subject)),name)
    return null
endfunction
function foo takes nothing returns nothing
    local location loc = GetHandle_location(nu,"foo")
endfunction
Attached Files
File type: 7zjgen-0.1.7z (229.3 KB)