Incomplete draft: do not cite!

Up to now, we've looked almost exclusively at tasks that are defined by methods.  However, we've seen a couple of exceptions, such as the Write and Not predicates, which aren't defined by methods.  They're callable by Step, but they're not they're not written in Step.1  Rather, they are a core part of the Step system itself.  Most programming languages have something like these, and they get referred to as primitives or "built-ins."  The term “primitive” here doesn't mean the task is rudimentary or limited, but rather that it is indivisible into smaller tasks.2

Primitive tasks bring up issues we haven't had to grapple with before.  If you're just starting to learn about declarative programming, the primary thing you should be aware of is that primitive tasks often have restrictions on what parameters are allowed to be variables, or conversely, on what parameters are allowed not to be variables.

That's all you really need to know.  If you want to learn more, read on.  Otherwise, you can skip to the next section.

Input/output modes

When we wrote our Likes predicate:

# Try: [Likes tanya ?]
[predicate]
Likes tanya sushi.
Likes tanya burgers.
Likes tanya mexican.
Likes jayden burgers.
Likes jayden ethiopean.
# Kimiko likes everything
Likes kimiko ?.
# Everyone likes pizza.
Likes ? pizza.

we discussed a number of different ways of calling it:

  • [Likes tanya ?] asks what Tanya likes
  • [Likes ? pizza] asks who likes pizza
  • [Likes tanya ethiopean] asks if Tanya likes Ethiopean food

When a given parameter of the predicate is an unbound variable — a variable that hasn't yet been given a value — and is filled in by that predicate, that parameter acts as an output of the predicate.  When the parameter is a specific value like tanya or pizza, or is a variable that's already been given a value, then that parameter acts as in input to the task.  The way that parameters can switch freely between acting as inputs and outputs is a unique and distinguishing feature of logic-programming languages.

The input/output pattern of a call is sometimes called its mode:

  • [Likes tanya ?] calls Likes in input-output mode
  • [Likes ? pizza] calls it in output-input mode
  • [Likes tanya ethiopean] calls it in input-input mode
  • [Likes ?who ?what] calls it in output-output mode

Likes can work with any mode.  However, some tasks only work with the right combinations of inputs and outputs.  This is a frequent issue to be aware of with primitives.  For example, there are tasks that effectively roll dice for you.  When you call:

[RandomFloat min max number]

with specific values for min and max, it chooses a random number between them and binds its third parameter, number, to that value.

This only makes sense when min and max are inputs and number is an output.  If you call it with number with a specific value, you would be asking if number was the number it rolled.  Except it can roll four billion different numbers, so rolling any specific one is wildly improbable.  There are basically no use cases for number being an input and so if it ever is called with that parameter as an input, it's likely a mistake on the programmer's part.  So RandomFloat is designed to throw an exception when that happens.

Similarly, it's hard to think of sensible use cases for min or max being outputs.  We say that RandomFloat only works in the input-input-output mode.

Determinism

Many primitives, such as Write, do their work without making choices; they're called deterministic because their behavior is completely determined by their inputs.  Primitives that make choices are called nondeterministic because their behavior is only partly determined by their inputs.

Wildly impractical search spaces

Thus far, we've studiously avoided making you learn the specific algorithm Step uses to find solutions to your queries.  But we've hinted that it involves a great deal of trial and error, which it does.  It's a little smarter than this, but if you think of it as trying every path in the choice tree, from left-to-right, you aren't too far off.  That means you can't have choices points with millions of options.  It's just not practical. 

This fact of life drives the design of a number of primitive tasks.  Some tasks are more limited than you might expect because the alternative is impractical.  For example:

  • RandomFloat, discussed above, is deterministic.  It doesn't try every possible random number until it finds one that works, because for many problems that would effectively run forever.
  • The < predicate, which checks whether one number is less than another, only works in input-input mode.  The alternative would involve it trying every possible pair of numbers.  Again, that's wildly impractical.

We'll now talk about a number of built-in tasks you might find useful.  However, if you aren't specifically interesting in learning more about Step, you mighter prefer to skip it and move on to the next section.

Notes


  1. Step itself is written in the C# language, so primitives are also written in C#.

  2. Another term that gets used is atomic, since the meaning of the Greek root from which that word comes means "indivisible."  However, that meaning has largely been lost in contemporary English, since what we previously thought to be the atoms of matter turn out to be divisible after all.