McCarthy's Ambiguous Operator (2005)
randomhacks.net
randomhacks.net
If you like this, you might also enjoy:
- Prolog: This is an entire language built around choice and backtracking. https://en.wikipedia.org/wiki/Prolog
- The Reasoned Schemer, which builds a Prolog-in-Scheme from first principles. This book is excellent, or even mind-blowing if you're never seen this before. https://mitpress.mit.edu/books/reasoned-schemer
- Oz/Mozart: This language takes the basic idea even further, adding concurrency and constraint solving. There's an older book about this that's nice: https://www.info.ucl.ac.be/~pvr/book.html It feels like Oz was an evolutionary dead end, but an interesting one.
- Probability monads. You can extend this general idea to include probability distributions over values. I have an older series of blog posts at http://www.randomhacks.net/probability-monads/ that explains this idea piece-by-piece, but there's been a ton of research in the past decade on probabilistic languages: http://www.probabilistic-programming.org/wiki/Home
It's definitely a fun topic!
It supports first class macros, continuations, and has the amb "operator" (implemented using a macro of course!): https://github.com/Patient0/FirstClassLisp/blob/master/Lisp/...
do x <- [1, 2, 3]
y <- [4, 5, 6]
if x * y == 8 then pure (x, y)
else []
which can be equivalently reworded as the list comprehension: [(x, y) | x <- [1, 2, 3], y <- [4, 5, 6], x * y == 8]
i.e., the underlying structure of amb without side-effects can also be understood as just a search of the input space for values that fulfill some criteria, rather than as backtracking via continuations.`amb` merges the assignments and predicate - this is important because you may not know your argument count statically, i.e. if your arguments are a list. But beyond that, `amb` may backtrack an arbitrarily deep call stack, so the the error return does not need to be expressed explicitly. You may think that is good or bad but it is not the same as the Haskell solution.
-- Lifts a list into Amb.
amb :: [a] -> Amb a
If we assume Amb is just List, then: amb = id
If we write the example in the original article in desugared style, we get: amb [1, 2, 3] >>= \x ->
amb [4, 5, 6] >>= \y ->
if x * y /= 8 then amb [] else pure () >>
pure (x, y)
(we are forced to use an awkward condition with `pure ()` on the else branch when calling `amb` because Haskell requires us to return values on all branches. We can rewrite it equivalently in terms of `when` to hide that detail: amb [1, 2, 3] >>= \x ->
amb [4, 5, 6] >>= \y ->
when (x * y /= 8) (amb []) >>
pure (x, y)
It now looks more similar to the original example.)It ends up looking like continuation-passing style: conceptually, if we encounter `amb []` in the nested function, the nested computation ends and we start examining the next value in the list values being assigned to `x`: the implementation given in the original post does end up using callcc, so with some imagination you might be able to derive some kind of equivalence here :)
The Haskell version requires each callee to be annotated with the `Amb` return type. It cannot be used to escape unless the caller is prepared for it to escape. That's a significant limitation (but all we're really saying is that Haskell doesn't support call/cc).
You still get credit for solving the basic problem but let's not be misleading.
Also don't forget to call head.
Of course Haskell list is a built-in type, but it's not magical except for the special bracket syntax. You can make a sugar-free user-defined type
data List a = Nil | Cons a (List a)
if you want.As an analogy let's say I define "(" to do write to stdout, and "print" as id. Even though "print(4)" works as expected, my "print function" is a lie.
Or in C source code where adjacent strings get merged, I could [#define CONCAT ] so that ["ab" CONCAT "cd"] gets preprocessed to ["ab" "cd"] and parsed as ["abcd"]. But I didn't actually write a concatenation operator. The concatenation happens because of something else, it would happen even if you didn't use CONCAT, and you can sprinkle CONCAT all over your source code with no effect.
foo = do
x <- [1,2 3]
y <- [1,2,3]
guard (x * y == 8)
return (x, y)I've observed several smart people struggling at the outset with the connection between recursively defined lists and nondeterminism in Haskell. It's not uncommon for people to assume that there is some essential connection between the two, because that's what "list monad" seems to suggest. In fact any collection type would do.
"Here, have some inputs. From those, find me the ones that satisfy this goal."
I feel like anyone actually implementing prolog will need to fully understand horn logic. AMB is a useful operator when the specifics of the backtracking search can be abstracted away.
But an "actual" prolog implementation needs to have a well-defined depth-first search over the horn-logic clauses. There are a lot of optimizations to make horn-logic solvers way faster.
Suppose you do the following:
(define a-continuation (call-with-current-continuation (lambda (c) c)))
The value of a-continuation at this point is a continuation object that, when called with argument x, sets a-continuation to x and returns to toplevel.
So if you now do, say,
(define (showfibs a b) (print a) (if (> a 1000) (a-continuation a) 1) (showfibs b (+ a b)))
and
(showfibs 0 1)
then you'll get 011235813213455891442333776109871597 as output, the new value of a-continuation will be 1597, and you'll be back at your top-level REPL prompt.
I don't think anything you can do with Python generators has such a pervasive ability to mess with control flow.
Python and c#’s yield are nothing like that. It’s just syntactic sugar for iterators.
https://mitpress.mit.edu/sites/default/files/sicp/full-text/...
All in all, this seems like a fun hack, but a catastrophically bad idea in practice.
https://bugs.ruby-lang.org/issues/10548
It looks like it's still included at the moment, though: