Skip to content

Instantly share code, notes, and snippets.

@routevegetable
Last active April 23, 2016 19:07
Show Gist options
  • Save routevegetable/c8258bb8e1b1263be440965d89521472 to your computer and use it in GitHub Desktop.
Save routevegetable/c8258bb8e1b1263be440965d89521472 to your computer and use it in GitHub Desktop.
A conversational free monad in java
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?"));
}
}
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