gracelang / minigrace

Self-hosting compiler for the Grace programming language
39 stars 22 forks source link

option module #228

Closed KimBruce closed 7 years ago

KimBruce commented 7 years ago

I'd like to propose a redesign of the option module. [Note: I understand that these modules are not particularly key to the language design, but ...] Currently the code is:

type Option⟦T⟧ = type {
    value -> T
    do(action:Block1⟦T, Done⟧) -> Done
    isSome -> Boolean
    isNone -> Boolean
}
class some⟦T⟧(contents:T) -> Option {
    method value -> T { contents }
    method do(action:Block1⟦T, Done⟧) -> Done { action.apply(value) }
    method isSome -> Boolean { true }
    method isNone -> Boolean { false }
}
class none⟦T⟧ -> Option {
    method value -> T { ProgrammingError.raise "none has no value" }
    method do(action:Block1⟦T, Done⟧) -> Done { done }
    method isSome -> Boolean { false }
    method isNone -> Boolean { true }
}

This encourages a style of first checking the presence of a value with isSome or isNone and then performing an action. I.e., if "do" is applied blindly it can result in a "ProgrammingError" exception. I'd like to propose a simplification that would force the user to confront the possibility of "none" before gaining access. I'd propose replacing the "do" method by

method ifSome (action:Block1⟦T, Done⟧) else (noneAction: Block0⟦Done⟧) -> Done 

The intuitive semantics is that if the receiver has a value, apply action to it, otherwise apply the noneAction. I considered making it more functional, by writing instead:

method ifSome [[U]](action:Block1⟦T, U⟧) else (noneAction: Block0⟦U⟧) -> U 

but decided to keep it in the simpler form (though I could be convinced otherwise). An important advantage of this is that we could drop all the other methods of the type and classes. Thus everything above could be replaced by:

type Option⟦T⟧ = type {
    ifSome (action:Block1⟦T, Done⟧) else (noneAction: Block0⟦Done⟧) -> Done
}

class some⟦T⟧(contents:T) -> Option {
    method ifSome(action:Block1⟦T, Done⟧) else (noneAction: Block0⟦Done⟧) -> Done { 
        action.apply(contents) 
    }
}

class none⟦T⟧ -> Option {
    method ifSome (action:Block1⟦T, Done⟧) else (noneAction: Block0⟦Done⟧) -> Done { 
        noneAction.apply
    }
}

Here is a simple program that uses this new library (called option2):

import "option2" as opt

def vals: List⟦Number⟧ = list[1,2,3,4,5]

method lookfor(n: Number) → opt.Option⟦Number⟧ {
    for (vals) do {k: Number →
        if (n==k) then {return opt.some(k)}
    }
    return opt.none
}

lookfor(5).ifSome{j: Number →
    print "found {j}"
} else {
    print "not found"
}

Note that methods like isSome and isNone are easily definable from ifSome()else()

Interestingly, this style is closer to that Andrew advocated for the collection classes with methods like find()ifNone() and at()ifAbsent(). He convinced me (eventually) that this was a good style, hence these suggested changes!

apblack commented 7 years ago

@KimBruce writes:

This encourages a style of first checking the presence of a value with isSome or isNone and then performing an action. I.e., if "do" is applied blindly it can result in a "ProgrammingError" exception.

That's not correct. The whole purpose of do is to avoid the need to check. It behave very much like do on a collection: if there is no value, do does nothing, and does not raise ProgrammingError. If you look, there is exactly one circumstance that raises ProgrammingError: asking none for its value.

That said, I think that it's a good idea to add the second method that you propose:

method ifSome ⟦U⟧ (action:Block1⟦T, U⟧) else (noneAction: Block0⟦U⟧) -> U 

The advantage, as I see it, is that it avoids the need for a temporary variable. Comparing:

lookfor(5).ifSome{j: Number →
    print "found {j}"
} else {
    print "not found"
}

without ifSome(_)else(_)

def result = lookfor(5)
if (result.isSome) then {
    print "found {result.value}"
} else {
    print "not found"
}

I suggest that ifSome(_)ifNone(_) is a better name, though, and that we include the variant ifNone(_)ifSome(_) as well.

Why the second version, the one that returns a value? Because if(_)then(_)else(_) returns a value, and we want them to be parallel.

I think that I'm also in favor of following Scala and having Option support the collection protocol.

apblack commented 7 years ago

I've made options also be sequences, so that they do follow the collections protocol.

In so doing, I've had to implement isEmpty, which is identical to isNone. This made me wonder if we should just replace isNone by isEmpty, rather than defining both. The complement ought then be called isFull.

In other words, should our options have constructors empty and full (rather than none and some) and actuators ifEmpty(_)ifFull(_), etc.?

While pondering names, I remembered that the convention in Smalltalk is to use the suffix do on an if... name if the block gets an argument. So ifEmpty(_)ifFull(_) would become ifEmpty(_)ifFullDo(_). Do we like that?

KimBruce commented 7 years ago

I don't see the point of making the two pieces non-parallel (one has a do and the other does not). Reasonable choices seem like:

ifEmpty(_)ifFull(_)
ifEmptyDo(_)ifFullDo(_)
ifEmptyThen(_)ifFullThen(_)

I personally prefer the first or third, as students are already used to if-then, but could live with the middle one.

apblack commented 7 years ago

Sorry, I wasn't very clear.

The point in using non-parallel names is that the clauses are not parallel. The full block gets an argument, and the empty block does not. The Do suffix is intended to remind the user that the block needs a parameter, which will be bound to the object to which they can do something — just like the internal iterator.

To be consistent, we should also change while(_)do(_) to while(_)repeat(_); in contrast, for(_)do(_) is already named according to this convention.

apblack commented 7 years ago

Discussed with @KimBruce today. Agreed that ifFull / isFull / ifEmpty / isEmpty are fine. Agreed not to use the Do suffix.

Add valueIfAbsent(fun) as method on options — returns value or the result of applying fun if there is no value.

apblack commented 7 years ago

This has been implemented in commit 36daea262. Still needs to be documented!

For consistency with the full–empty nomenclature, valueIfAbsent is named valueIfEmpty

apblack commented 7 years ago

Now documented.