mlhaufe / brevity

Brevity is a library that enables Feature-Oriented Programming (FOP) and solves the expression problem in a manner that makes data and operation declarations trivial to define and compose.
GNU Affero General Public License v3.0
1 stars 0 forks source link

Migrate to a full Object-Algebra approach #53

Closed mlhaufe closed 1 year ago

mlhaufe commented 1 year ago

The current approach with traits is unacceptable when it comes to interacting features:

const printEval = Trait(Exp, {
    Lit({value}){ return value.toString() },
    Add(self) {
        const {left, right} = self
        return `${print(left)} + ${print(right)} = ${evaluate(self)}`
    }
})

print and eval would be the declarations in scope and not associated with the appropriate subtype that can be declared elsewhere. If Exp was extended to MulExp with its own evaluate and print, they would not be called.

Traits need to be more late-bound than they are currently. The best way to accomplish this is to treat them as methods:

const printEval = Trait(Exp, {
    Lit({value}){ return value.toString() },
    Add(self) {
        const {left, right} = self
        return `${left.print()} + ${right.print()} = ${self.evaluate()}`
    }
})

This breaking change requires a significant change in the implementation. A full Object-Algebra approach is needed, but that doesn't mean the overhead of the typical approach is required. I see no need for Trait to be defined as a factory of records.

Here is how I envision the new approach:

Data

const ExpAdd= data({
    Lit: { value: {} },
    Add: { left: {}, right: {} }
})

Data -> data

// Data Extension
const ExpBool = data({
  [extend]: ExpAdd,
  Bool: { value: {} },
  Cond: { pred: {}, ifTrue: {}, ifFalse: {} }
})

Nothing new here

Trait

const Printable = trait(ExpAdd, {
  Lit({value}){ return `${value}` }.
  Add({left, right}) { return  `${left.print()} + ${right.print()}` }
})
  1. Trait -> trait
  2. Notice the use of methods instead of function calls.
// Extension
const BoolPrintable = trait(ExpBool, {
    [extend]: Printable,
    Bool({ value }) { return `${value}` },
    Cond({ pred, ifTrue, ifFalse }){ return `if (${pred.print()}) then ${ifTrue.print()} else ${ifFalse.print()}` }
})

Nothing new here

memoFix

With the new approach to traits, memoFix won't work as-is. This could become a symbol option instead:

const MyTrait = trait(FooData, {
    [memoFix]: { bottom: (self) => ... },
    Foo(self){ ... }
})

Complect

By treating traits as methods instead of functions, it's necessary to have a merging concept to combine the data and trait declarations.

This will be handled by the complect function

const exp = complect(ExpBool, { print: BoolPrintable })

const {Lit, Add} = exp

Lit(3).print() // 3
Add(Lit(3), Lit(4)).print() // 3 + 4

This approach provides the added benefit of reducing the import/export overhead down to the complected object