I think Scala is an excelent language and that it will influence future languages greatly. Having said that I agree with Pasko Robert who said he prefered Multiple Dispatch to patterns. The patterns example is coded below using my Pattern Enforcing Compiler (PEC):
that implements the Multiple Dispatch pattern on top of standard Java.
publicinterface TermMD extends pec.compile.multipledispatch.MultipleDispatch { // The methods in this interface are Multiply Dispatched
Term binarySimplify( Term toTheLeft, Term toTheRight );
Term unarySimplify( Term toTheRight );
}
publicinterface Term extends TermMD { // The method in this interface is Singly Dispatched
Term simplify();
}
publicabstractclass TerminalTerm implements Term {
publicfinal Term binarySimplify( Term toTheLeft, Term toTheRight ) { thrownew AssertionError(); } // Method body replaced by PEC - with a multiple dispatch call
publicstaticfinal Term binarySimplify( final Term kind, final Term toTheLeft, final Term toTheRight ) { return kind; } // Default simplifier for unary terms - do nothing
publicfinal Term unarySimplify( Term toTheRight ) { thrownew AssertionError(); } // Method body replaced by PEC - with a multiple dispatch call
publicstaticfinal Term unarySimplify( final Term kind, final Term toTheRight ) { return kind; } // Default simplifier for binary terms - do nothing
public Term simplify() { returnthis; } // Nothing to simplify for a terminal!
}
publicabstractclass UnaryTerm extends TerminalTerm {
protectedfinal Term toTheRight;
protected UnaryTerm( final Term toTheRight ) { this.toTheRight = toTheRight.simplify(); }
public Term simplify() { return unarySimplify( toTheRight ); }
}
publicabstractclass BinaryTerm extends UnaryTerm {
protectedfinal Term toTheLeft;
protected BinaryTerm( final Term toTheLeft, final Term toTheRight ) {
super( toTheRight );
this.toTheLeft = toTheLeft.simplify();
}
public Term simplify() { return binarySimplify( toTheLeft, toTheRight ); }
}
publicfinalclass Zero extends TerminalTerm {}
publicfinalclass NaN extends TerminalTerm {}
publicfinalclass Var extends TerminalTerm {}
publicfinalclass Mult extends BinaryTerm {
public Mult( final Term toTheLeft, final Term toTheRight ) { super( toTheLeft, toTheRight ); }
publicstaticfinal Term binarySimplify( final Mult m, final Zero z, final Term t ) { return z; } // 0 * x = 0
publicstaticfinal Term binarySimplify( final Mult m, final Term t, final Zero z ) { return z; } // x * 0 = 0
publicstaticfinal Term binarySimplify( final Mult m, final Zero z, final Zero z2 ) { return z; } // 0 * 0 = 0, needed to resolve conflict
publicstaticfinal Term binarySimplify( final Mult m, final NaN n, final Term t ) { return n; } // NaN * x = NaN
publicstaticfinal Term binarySimplify( final Mult m, final Term t, final NaN n ) { return n; } // x * NaN = NaN
publicstaticfinal Term binarySimplify( final Mult m, final NaN n, final NaN n2 ) { return n; } // NaN * NaN = NaN, needed to resolve conflict
publicstaticfinal Term binarySimplify( final Mult m, final NaN n, final Zero z ) { return n; } // NaN * 0 = NaN, needed to resolve conflict
publicstaticfinal Term binarySimplify( final Mult m, final Zero z, final NaN n ) { return n; } // 0 * NaN = NaN, needed to resolve conflict
}
The example is slightly expanded to show how conflicts are handelled. In particular 0 * 0, 0 * NaN, NaN * 0, and NaN * NaN. The multiple dispatch methods, the static mathods called binarySimplify and unarySimplify in the code can be in any class and can be seperately compiled. EG the power example:
publicfinalclass Power extends BinaryTerm {
public Power( final Term toTheLeft, final Term toTheRight ) { super( toTheLeft, toTheRight ); }
publicstaticfinal Term binarySimplify( final Mult m, final Power pL, final Power pR ) { // x**z * y**z = (x * y)**z
if ( pL.toTheRight == pR.toTheRight ) return (new Power( new Mult( pL.toTheLeft, pR.toTheLeft ), pL.toTheRight )).simplify();
return m;
}
}
> > I guess I'm not really complaining about the > > pattern-matching but more wishing for something. > > > > PS. If what i am talking about here is already possible > in > > Scala, I'd love to be enlightened. > > If I understand you correctly, you want to be able to > combine independent extensions of data and operations. > You can do this with mixin composition. Let's assume we > have a base system of algebraic terms, with a simplifier.
I think I'm not making myself clear. This is probably a terrible idea but what if case classes could have mixin-methods? What I mean is instead of a separate Simplifier class, each case class would implment the simplification terms that applied to it (obviously some of the terms appy to mutliple types of term and that's a serious issue, I think). Then when the simplification method is called, all the term classes' simplification methods would be called (order? I don't know)
This would provide the same functionality but keep things very OO (as I am familiar with it.) Hopefully this explains my thoughts better but I haven't got a really complete solution to describe.
With the multiple dispatch demonstrated in the post above yours you can do what you want. The simplifier methods can be in any class and in any order. The methods can be added to at any time whilst the program is running by loading a class that contains more methods, i.e. referencing a new class, a class from another machine, etc.
Ive been working on something similar myself, trying to leverage C# 3.0 features.
Unfortunately, it isnt possible to convert anonymous methods with statement blocks into expression trees, so the following sample wouldnt work with C# 3.0 as it stands.
Its a bit ugly, but heres a sample of how it might look:
Matcher match = new Matcher( (int a, int b, int c) => Case(Foo(Bar(Baz(a,b,c),4,_))) >> ()=>{ Foo foo = new Foo(); for (int i = a; i < b; i += c) foo.Add(new Bar(i)); return foo; }), (int a) => Case(Foo(Bar(Baz(a,_,_),_,7)))) >> ()=>Foo(a), (Baz x) => Case(Foo(Bar(x),_,_))) >> ()=>x );
<pre class="literal-block"> class Term case class Num(n: int) extends Term case class Var(name: String) extends Term case class Mul(l: Term, r: Term) extends Term case class Add(l: Term, r: Term) extends Term
def simplify(term: Term) = term match { case Mul(Num(0), x) => Num(0) case Mul(Num(1), x) => x case _ => term } </pre>
Extensibility analysis: If you add another subclass of Term, most patterns associated with Term will need another case and will break. Poor extensibility.
Encapsulation analysis: If class Num n field changes, it will affect constructor parameters and the simplify pattern will break. If class Mul l field changes, it will affect constructor parameters and the simplify pattern will break. If class Mul r field changes it will affect constructor parameters and the simplify pattern will break. All other patterns associated with Term using Term subclasses constructors will break too. If class Term changes everything will break. (an OO issue, not a pattern matching issue) Poor encapsulation.
Alternative pure OO aproach using polymorphism and true delegation: <pre class="literal-block"> public class Term { Term simplify() {return this;} Term simplifyMul(Mul mul) {return mul;}}
class Num extends Term {int n; Num(int n) {this.n = n;} Term simplifyMul(Mul mul) { if (n == 0) {return new Num(0);} else if (n == 1) {return mul.getR();} else {return mul;}}}
class Var extends Term {String name; Var(String name) {this.name = name;}}
class Mul extends Term {Term l; Term r; Mul(Term l, Term r) {this.l = l; this.r = r;} Term getR() {return r;} Term simplify() {return l.simplifyMul(this);}}
class Add extends Term {Term l; Term r; Add(Term l, Term r) {this.l = l; this.r = r;}} </pre>
Extensibility analysis: If you add another subclass of Term, coupling will be done through method overriding and won't break other classes. Better extensibility.
Encapsulation analysis: If class Mul r field changes it will break class Num, but thats it. If class Term changes everything will break. Encapsulation is not perfect here neither, but is a lot better than the pattern matching example.
The OO example was written in Java and so it has a lot more lines of code than the Scala example, but it is a Java issue, not an OO issue.
And NO, the Visitor pattern is not needed in this case, as it is not needed in most cases. Basic OO is enough most of the time.
> Pattern matching is much maligned by object-oriented > programmers. I wonder why?
The reason is that some explanation is missing when pattern matching makes sense and when not. It seems like case classes make most sense when you have a finite number of possible cases defined in a simple hierarchy, much like an enumeration. The sample you make fits very well into this category. But when reading your article you get the impression that case classes are considered universal without limitations and the respective OO approach is not only redundant but also inferior.
If I develop a very sophisticated BTree, because I want to implement a database, I have no worries that the OO solution will give me the extensibility I need. I'm not sure here about case classes. I think somethink like this is what many OO programmers think when they read articles that say something like case classes are best. Some explanation is required what case classes are considered good for, pros and cons.
Flat View: This topic has 35 replies
on 3 pages
[
«
|
123
]