In Nemerle you can use the traditional approach of defining
hierarchies of classes. Then the hierarchy can be easy extended. For
defining new operations you can use matching over types, similar to
matching over variants:
match (e)
| t is Class1 => t.field_of_class1
| t is Class2 => ...
| _ => some default
Are there any other mechanisms Scala provides?
<plug>
Also, from my (limited) experience, it is not always all that good
idea to make everything extra-extensible, with proper access
restrictions, properties and the like, because you spend more time
doing that than solving the actual problem. I prefer to focus on
getting things done, which is most of the time easier in functional
languages than in object oriented ones, where you need to think about
all that inheritance stuff. YMMV.
</plug>
--
Michał
But then, say, when you add Class3, you should change every operation
over your "variant" to take into account this new case. In fact, doing
isa "pattern matching" on normal classes combines the worst of
functional and oo customary approaches to the expression problem: 1)
you cannot add new cases to the variant-like hierarchy without
changing your operations (by addition of a new case) and 2) -assuming
your match expression likely form part of the implementation of some
method m1 in a superclass, say, Class, from which both Class1 and
Class2 inherit- you can't conveniently add new methods m2, m3, ...
that work for every subclass (kinda variant cases in this context)
without changing Class. So in this case, oo flavor, I would implement
the cases to some extent as you suggest, by means of subclasses of a
common "variant" class, but then I would implement operations as
different methods m1, m2, ... for Class1 and Class2 which override
virtual methods m1, m2, ... specified for Class (avoiding case-by-case
analysis); this way you can at least add new cases, v.gr Class3, with
relative ease. Also, avoding the runtime downcasts implied by isa
case-by-case analysis,as in your example, is common place in oo
programming (the typical example being that of widgets which know by
themselves how they should be drawn, instead of relying on a unique
draw() implementation that dispatch on a case-by-case basis to a
routine appropiate for drawing each specific widget type taken from a
limited, non-extensible, set of widgets types). On the other hand, fp
promotes operation extensibility to the detriment of data
extensibility, by means of variant types and pattern matching on their
constructors. The expression problem refers to the possibility of
being open to extensibility on both fronts (data and operations)
without losing static typeability.
Regards,
Carlos
I think you should always consider what kind of task you need to do:
is it more data-oriented (there are complex dependencies between data,
many levels of common structures to be reused) or operation-oriented
(your algorithms are complex and often use the same auxiliary methods,
data they operate on is mainly just a way to describe some state or
structure of real-world data).
In first case using classes and subtyping of your own is the best
idea. And one more point I don't quite get - why do everybody consider
adding new operation so difficult in standard C#/Java model? You can
just define virtual method in superclass with default implementation
and that's all - then you can override this method in one or two
subclasses where you really need it. And if you really need new
operation to be handled for ALL data-types you declare it abstract and
compiler forces you to implement it everywhere - a very convenient
way, just jump through error reports from compiler.
In second case you use variants (and hey, they should be closed!) - if
you need to add new data type you again gets warned by compiler about
places in code, where you need to implement something. And you again
have the choice to disable this feature by defining default handler in
some operations (just like we used virtual but non-abstract method
previously).
How do you see the scenario, where you need to add some logic, but you
don't bother implementing it...? ;)
But ok, I will read some more, maybe it is possible to have the data
and operation centered code at the same time?
--
Kamil Skalski
http://nazgul.omega.pl
This approach is in general possible with our implementation of
variants. I was thinking about two approaches:
variant Expr {
| Lit { x : int; }
| Neg { e: Expr }
| Plus { e1: Expr; e2 : Expr }
public Eval () : int {
match (this) {
| Lit (n) => n
| Neg (e) => -e.Eval()
| Plus (e1,e2) => e1.Eval() + e2.Eval()
}
}
}
could be automatically transformed (by a macro specified by user, or
maybe automatically executed by compiler) into:
variant Expr {
| Lit { x : int; public override Eval () : int { x } } // not
possible at source code level, but possible to do by macro
| Neg { e: Expr; public override Eval() : int { ... } }
| Plus { e1: Expr; e2 : Expr; public override Eval() : int { ... } }
public abstract Eval () : int;
}
this way we get rid of typecasts and typechecks - they are done by
runtime through virtual dispatch.
The other approach I was thinking recently about would be to generate
the visitor interface by a macro together with its calling inside all
the variant cases. This way again one could implement operations on
variants in a OO way, without need to implement visitor pattern by his
own.
Of course this does not solve the "expression problem" in a way I read
about it - to add operations AND variant cases in an easy parallel
way, without modifying existing code.
You could of course use some of the ideas from Scala and others to
have these "generations" of data type, each being richer by some
operation / variant case from previous one. This approach seems to
have many advantages, though I usually prefer to use variants in an
uniform way throughout the whole program - with closed variants
everything is straightforward and implementation is not distributed
into tons of strange subclasses. :)
I think adding the extend able version of variants concept should be
even possible to do by a macro, thus as an extension of language.
> draw() implementation that dispatch on a case-by-case basis to a
> routine appropiate for drawing each specific widget type taken from a
> limited, non-extensible, set of widgets types). On the other hand, fp
> promotes operation extensibility to the detriment of data
> extensibility, by means of variant types and pattern matching on their
> constructors. The expression problem refers to the possibility of
> being open to extensibility on both fronts (data and operations)
> without losing static typeability.
>
> Regards,
> Carlos
>
> >
>