Last active
April 23, 2016 19:07
-
-
Save routevegetable/c8258bb8e1b1263be440965d89521472 to your computer and use it in GitHub Desktop.
A conversational free monad in java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.util.Arrays; | |
import java.util.LinkedList; | |
import java.util.List; | |
import java.util.function.Function; | |
public abstract class ChatterMonad<T> { | |
/** | |
* A result. | |
* If deferred, a value can be found in 'yieldValue', and a continuation can be found in 'next'. | |
* If not, the result object is in obj. | |
*/ | |
public static class Result<T> { | |
public final T obj; | |
// The deferred section | |
public final boolean deferred; | |
public final String yieldValue; | |
public final ChatterMonad<T> next; | |
public Result(String yieldValue, ChatterMonad<T> next) { | |
this.deferred = true; | |
this.next = next; | |
this.yieldValue = yieldValue; | |
this.obj = null; | |
} | |
public Result(T obj) { | |
this.obj = obj; | |
this.deferred = false; | |
this.yieldValue = null; | |
this.next = null; | |
} | |
} | |
/** | |
* Execute this monad until a yield occurs. | |
* @param is The 'environment' to use in execution. | |
*/ | |
protected abstract Result<T> go(String is); | |
/** | |
* Monadic bind operator | |
*/ | |
public final <U> ChatterMonad<U> BIND(final Function<T, ChatterMonad<U>> f) { | |
return new ChatterMonad<U>() { | |
/** | |
* Given a possibly-deferred result<T>, return a possibly-deferred result<U> | |
* The supplied InputStream is used if a is not deferred | |
*/ | |
private Result<U> step(final Result<T> a, final String is) { | |
if(!a.deferred) { | |
return f.apply(a.obj).go(is); | |
} else { | |
return new Result<>(a.yieldValue, new ChatterMonad<U>() { | |
protected Result<U> go(String is2) { | |
return step(a.next.go(is2), is2); | |
} | |
}); | |
} | |
} | |
protected Result<U> go(String is) { | |
return step(ChatterMonad.this.go(is), is); | |
} | |
}; | |
} | |
public <U> ChatterMonad<U> LIFT(Function<T, U> f) { | |
return BIND(a -> Immediate(f.apply(a))); | |
} | |
public final static <T> ChatterMonad<T> Immediate(final T v) { | |
return new ChatterMonad<T>() { | |
protected Result<T> go(String is) { | |
return new Result<T>(v); | |
} | |
}; | |
} | |
public final static ChatterMonad<Void> Yield(final String toYield) { | |
return new ChatterMonad<Void>() { | |
protected Result<Void> go(String is) { | |
return new Result<>(toYield, new ChatterMonad<Void>() { | |
protected Result<Void> go(String is) { | |
return new Result<>(null); | |
} | |
}); | |
} | |
}; | |
} | |
public final static <O> ChatterMonad<String> CURRENT_INPUT() { | |
return new ChatterMonad<String>() { | |
protected Result<String> go(String is) { | |
return new Result<>(is); | |
} | |
}; | |
} | |
/** | |
* Yield an object, receive an object. | |
*/ | |
public final static ChatterMonad<String> Question(final String toYield) { | |
return Yield(toYield).THEN(CURRENT_INPUT()); | |
} | |
/** | |
* Combine this and another chatter monad, throwing away the intermediate result. | |
*/ | |
public <U> ChatterMonad<U> THEN(final ChatterMonad<U> b) { | |
return BIND(v -> b); | |
} | |
/** | |
* Append an immediate value | |
*/ | |
public <U> ChatterMonad<U> THEN_RETURN(final U b) { | |
return THEN(Immediate(b)); | |
} | |
/** | |
* Follow this with another if result is not null | |
*/ | |
public <U> ChatterMonad<U> NOT_NULL(final ChatterMonad<U> b) { | |
return BIND(value -> value != null ? b : Immediate(null)); | |
} | |
/* -------------------- Misc: -------------------- */ | |
/** | |
* ChatterMonad<T> -> ChatterMonad<Boolean> | |
* Yield true if result is equal to given value. False otherwise. | |
*/ | |
public final ChatterMonad<Boolean> EQ(final T value) { | |
return LIFT(in -> (value == in) || (value != null && value.equals(in))); | |
} | |
public final static <T> ChatterMonad<List<T>> SEQUENCE(ChatterMonad<T>... ds) { | |
return SEQUENCE(Arrays.asList(ds)); | |
} | |
public final static <T> ChatterMonad<List<T>> SEQUENCE(final List<ChatterMonad<T>> ds) { | |
if(ds.size() == 0) { | |
return Immediate((List<T>)new LinkedList<T>()); // Empty list | |
} else { | |
return ds.get(0) | |
.BIND(first -> SEQUENCE(ds.subList(1, ds.size())) | |
.LIFT(rest -> { | |
final List<T> l = new LinkedList<>(); | |
l.add(first); | |
l.addAll(rest); | |
return l; | |
})); | |
} | |
} | |
/** | |
* Run multiple branches until one is found which yields non-null | |
*/ | |
public final static <T> ChatterMonad<T> FIRST_NOT_NULL(final List<ChatterMonad<T>> ds) { | |
if(ds.size() == 0) { | |
return Immediate(null); | |
} else { | |
return ds.get(0).BIND(first -> | |
first != null ? | |
Immediate(first) : | |
FIRST_NOT_NULL(ds.subList(1, ds.size()))); | |
} | |
} | |
public final static <T> ChatterMonad<T> FIRST_NOT_NULL(ChatterMonad<T>... ds) { | |
return FIRST_NOT_NULL(Arrays.asList(ds)); | |
} | |
/** | |
* This 'executes' a ChatterMonad, returning the final result | |
* If a string is yielded which ends in a question mark, the continuation is called with | |
* a line form stdin. If any other string is yielded, the continuation is called with null. | |
*/ | |
private final static <T> T dialogue(ChatterMonad<T> m) throws IOException { | |
Result<T> r = m.go(""); | |
while(r.deferred) { | |
System.out.println(r.yieldValue); | |
if(r.yieldValue.endsWith("?")) { | |
r = r.next.go(Parser.LINE_STRING.parse(System.in).obj); | |
} else { | |
r = r.next.go(null); | |
} | |
} | |
return r.obj; | |
} | |
private final static ChatterMonad<String> ASK(String thing) { | |
return Question("What is your " + thing + "?"); | |
} | |
private final static ChatterMonad<Integer> GET_RETRY_INT(String text) { | |
final ChatterMonad<Integer>[] m = new ChatterMonad[1]; | |
return m[0] = Question(text).BIND(in -> { | |
try { | |
Integer age = Integer.parseInt(in); | |
return Immediate(age); | |
} catch(Exception e) { | |
return m[0]; // Recur...ew. | |
} | |
}); | |
} | |
private final static ChatterMonad<String> GET_PREFERENCE = GET_RETRY_INT("What is your age?").BIND(in -> | |
in < 16 ? Yield("Sorry, you're too young").THEN_RETURN(null) : | |
Question("Okay, what is your preference?")); | |
public final static ChatterMonad<String> GET_PREFERENCE_EXTENDED = GET_PREFERENCE.BIND(in -> | |
in == null ? Immediate(null) : | |
Question("And how much do you like " + in + "?") | |
.LIFT(prefLevel -> "likes " + in + " " + prefLevel)); | |
public static void main(String[] str) throws IOException { | |
dialogue(Yield("Hello, I am a thing")); | |
ChatterMonad<String> askNameAndGreet = ASK("name").BIND(in -> Yield("Hello, " + in).THEN_RETURN(in)); | |
List<String> result = dialogue(SEQUENCE(askNameAndGreet, askNameAndGreet, ASK("age"))); | |
System.out.println("Got result of dialogue: " + result); | |
Object result2 = dialogue(GET_PREFERENCE_EXTENDED); | |
System.out.println("Got result of dialogue: " + result2); | |
dialogue(GET_RETRY_INT("Please enter an integer?")); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.util.Arrays; | |
import java.util.LinkedList; | |
import java.util.List; | |
public abstract class ChatterMonad<T> { | |
/** | |
* A result. | |
* If deferred, a value can be found in 'yieldValue', and a continuation can be found in 'next'. | |
* If not, the result object is in obj. | |
*/ | |
public static class Result<T> { | |
public final T obj; | |
// The deferred section | |
public final boolean deferred; | |
public final String yieldValue; | |
public final ChatterMonad<T> next; | |
public Result(String yieldValue, ChatterMonad<T> next) { | |
this.deferred = true; | |
this.next = next; | |
this.yieldValue = yieldValue; | |
this.obj = null; | |
} | |
public Result(T obj) { | |
this.obj = obj; | |
this.deferred = false; | |
this.yieldValue = null; | |
this.next = null; | |
} | |
} | |
/** | |
* Execute this monad until a yield occurs. | |
* @param is The 'environment' to use in execution. | |
*/ | |
protected abstract Result<T> go(String is); | |
public abstract class Bind<U> extends ChatterMonad<U> { | |
protected abstract ChatterMonad<U> next(T in); | |
/** | |
* Given a possibly-deferred result<T>, return a possibly-deferred result<U> | |
* The supplied InputStream is used if a is not deferred | |
*/ | |
private Result<U> step(final Result<T> a, final String is) { | |
if(!a.deferred) { | |
return next(a.obj).go(is); | |
} else { | |
return new Result<>(a.yieldValue, new ChatterMonad<U>() { | |
protected Result<U> go(String is2) { | |
return step(a.next.go(is2), is2); | |
} | |
}); | |
} | |
} | |
protected Result<U> go(String is) { | |
return step(ChatterMonad.this.go(is), is); | |
} | |
} | |
public final static <T> ChatterMonad<T> Immediate(final T v) { | |
return new ChatterMonad<T>() { | |
protected Result<T> go(String is) { | |
return new Result<T>(v); | |
} | |
}; | |
} | |
public final static ChatterMonad<Void> Yield(final String toYield) { | |
return new ChatterMonad<Void>() { | |
protected Result<Void> go(String is) { | |
return new Result<>(toYield, new ChatterMonad<Void>() { | |
protected Result<Void> go(String is) { | |
return new Result<>(null); | |
} | |
}); | |
} | |
}; | |
} | |
public final static <O> ChatterMonad<String> CURRENT_INPUT() { | |
return new ChatterMonad<String>() { | |
protected Result<String> go(String is) { | |
return new Result<>(is); | |
} | |
}; | |
} | |
/** | |
* Yield an object, receive an object. | |
*/ | |
public final static ChatterMonad<String> Question(final String toYield) { | |
return Yield(toYield).THEN(CURRENT_INPUT()); | |
} | |
/** | |
* Lifts a function | |
*/ | |
public abstract class Lift<U> extends ChatterMonad<U> { | |
public final ChatterMonad<T>.Bind<U> b; | |
public Lift() { | |
b = ChatterMonad.this.new Bind<U>() { | |
protected ChatterMonad<U> next(T value) { | |
return Immediate(f(value)); | |
} | |
}; | |
} | |
@Override | |
public final Result<U> go(String is) { | |
return b.go(is); | |
} | |
protected abstract U f(T in); | |
} | |
/** | |
* Combine this and another chatter monad, throwing away the intermediate result. | |
*/ | |
public <U> ChatterMonad<U> THEN(final ChatterMonad<U> b) { | |
return this.new Bind<U>() { | |
protected ChatterMonad<U> next(T value) { | |
return b; | |
} | |
}; | |
} | |
/** | |
* Append an immediate value | |
*/ | |
public <U> ChatterMonad<U> THEN_RETURN(final U b) { | |
return this.new Bind<U>() { | |
protected ChatterMonad<U> next(T value) { | |
return Immediate(b); | |
} | |
}; | |
} | |
/** | |
* Follow this with another if result is not null | |
*/ | |
public <U> ChatterMonad<U> NOT_NULL(final ChatterMonad<U> b) { | |
return this.new Bind<U>() { | |
protected ChatterMonad<U> next(T value) { | |
if(value != null) | |
return b; | |
else | |
return Immediate(null); | |
} | |
}; | |
} | |
/* -------------------- Misc: -------------------- */ | |
/** | |
* ChatterMonad<T> -> ChatterMonad<Boolean> | |
* Yield true if result is equal to given value. False otherwise. | |
*/ | |
public final ChatterMonad<Boolean> EQ(final T value) { | |
return this.new Lift<Boolean>() { | |
protected Boolean f(T in) { | |
return (value == in) || (value != null && value.equals(in)); | |
} | |
}; | |
} | |
public final static <T> ChatterMonad<List<T>> SEQUENCE(ChatterMonad<T>... ds) { | |
return SEQUENCE(Arrays.asList(ds)); | |
} | |
public final static <T> ChatterMonad<List<T>> SEQUENCE(final List<ChatterMonad<T>> ds) { | |
if(ds.size() == 0) { | |
return Immediate((List<T>)new LinkedList<T>()); // Empty list | |
} else { | |
return ds.get(0).new Bind<List<T>>() { | |
protected ChatterMonad<List<T>> next(final T first) { | |
return SEQUENCE(ds.subList(1, ds.size())).new Lift<List<T>>() { | |
protected List<T> f(List<T> rest) { | |
final List<T> l = new LinkedList<>(); | |
l.add(first); | |
l.addAll(rest); | |
return l; | |
} | |
}; | |
} | |
}; | |
} | |
} | |
/** | |
* Run multiple branches until one is found which yields non-null | |
*/ | |
public final static <T> ChatterMonad<T> FIRST_NOT_NULL(final List<ChatterMonad<T>> ds) { | |
if(ds.size() == 0) { | |
return Immediate(null); | |
} else { | |
return ds.get(0).new Bind<T>() { | |
protected ChatterMonad<T> next(final T first) { | |
if(first != null) { | |
return Immediate(first); | |
} else { | |
return FIRST_NOT_NULL(ds.subList(1, ds.size())); | |
} | |
} | |
}; | |
} | |
} | |
public final static <T> ChatterMonad<T> FIRST_NOT_NULL(ChatterMonad<T>... ds) { | |
return FIRST_NOT_NULL(Arrays.asList(ds)); | |
} | |
/** | |
* This 'executes' a ChatterMonad, returning the final result | |
* If a string is yielded which ends in a question mark, the continuation is called with | |
* a line form stdin. If any other string is yielded, the continuation is called with null. | |
*/ | |
private final static <T> T dialogue(ChatterMonad<T> m) throws IOException { | |
Result<T> r = m.go(""); | |
while(r.deferred) { | |
System.out.println(r.yieldValue); | |
if(r.yieldValue.endsWith("?")) { | |
r = r.next.go(Parser.LINE_STRING.parse(System.in).obj); | |
} else { | |
r = r.next.go(null); | |
} | |
} | |
return r.obj; | |
} | |
private final static ChatterMonad<String> ASK(String thing) { | |
return Question("What is your " + thing + "?"); | |
} | |
private final static ChatterMonad<Integer> GET_RETRY_INT(String text) { | |
return Question(text).new Bind<Integer>() { | |
protected ChatterMonad<Integer> next(String in) { | |
try { | |
Integer age = Integer.parseInt(in); | |
return Immediate(age); | |
} catch(Exception e) { | |
return this; // Recur | |
} | |
} | |
}; | |
} | |
private final static ChatterMonad<String> GET_PREFERENCE = | |
GET_RETRY_INT("What is your age?").new Bind<String>() { | |
protected ChatterMonad<String> next(Integer in) { | |
if(in < 16) { | |
return Yield("Sorry, you're too young").THEN_RETURN(null); | |
} else { | |
return Question("Okay, what is your preference?"); | |
} | |
} | |
}; | |
public final static ChatterMonad<String> GET_PREFERENCE_EXTENDED = GET_PREFERENCE.new Bind<String>() { | |
protected ChatterMonad<String> next(final String in) { | |
if(in == null) | |
return Immediate(null); | |
return Question("And how much do you like " + in + "?").new Lift<String>() { | |
protected String f(String prefLevel) { | |
return "likes " + in + " " + prefLevel; | |
} | |
}; | |
} | |
}; | |
public static void main(String[] str) throws IOException { | |
dialogue(Yield("Hello, I am a thing")); | |
ChatterMonad<String> askNameAndGreet = ASK("name").new Bind<String>() { | |
protected ChatterMonad<String> next(String in) { | |
return Yield("Hello, " + in).THEN_RETURN(in); | |
} | |
}; | |
List<String> result = dialogue(SEQUENCE(askNameAndGreet, askNameAndGreet, ASK("age"))); | |
System.out.println("Got result of dialogue: " + result); | |
Object result2 = dialogue(GET_PREFERENCE_EXTENDED); | |
System.out.println("Got result of dialogue: " + result2); | |
dialogue(GET_RETRY_INT("Please enter an integer?")); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment