Skip to content

Higher-Order Predicates

Higher-order predicates take a goal (predicate or lambda) as an argument and apply it to list elements. Combined with lambdas, they give Clausal Prolog a functional programming feel.


Quick Example

double(X, Y) <- (Y == X * 2)

test("double all") <- (
    maplist(double, [1, 2, 3], [2, 4, 6])
)

test("keep evens") <- (
    include((X <- (X % 2 == 0)), [1, 2, 3, 4, 5, 6], [2, 4, 6])
)

Calling Goals

call/1..8 and call_goal/1..8

call(Goal) invokes a goal. call(Goal, A1, ..., AN) appends extra arguments. call_goal is an alias.

test("call/1") <- call((X <- (X == 42)), 42)

test("call/2") <- (
    call(((X, Y) <- (Y == X * 2)), 5, 10)
)

The goal can be a lambda, a predicate name, or a partial goal term (a cell such as add(1), which call extends with the extra arguments).


Mapping

maplist/2

maplist(Goal, List) — test Goal(Elem) for every element. Succeeds if the goal succeeds for all elements, and backtracks into every call, as the ISO prologue's definition by call/N does: with p(1), p(2), p(3), maplist(p, [X, Y]) has nine answers. On an OPEN list (unbound, or a partial list [a, *T]) it enumerates as that definition does, depth first and without end: maplist(p, L) answers L = [], L = [1], L = [1, 1], ...

positive(X) <- (X > 0)

test("all positive") <- maplist(positive, [1, 2, 3])

maplist/3

maplist(Goal, Xs, Ys) — transform each element via Goal(X, Y); like maplist/2 it backtracks into every call.

square(X, Y) <- (Y == X ** 2)

test("squares") <- (
    maplist(square, [1, 2, 3, 4], [1, 4, 9, 16])
)

With an inline lambda:

test("squares inline") <- (
    maplist(((X, Y) <- (Y == X ** 2)), [1, 2, 3, 4], [1, 4, 9, 16])
)

Filtering

include/3

include(Goal, List, Included) — keep elements where Goal(Elem) succeeds.

test("filter") <- include((X <- (X > 3)), [1, 5, 2, 8, 3], [5, 8])

exclude/3

exclude(Goal, List, Kept) — keep elements where Goal(Elem) fails. The inverse of include.

test("exclude") <- exclude((X <- (X > 3)), [1, 5, 2, 8, 3], [1, 2, 3])

filter_map/3

filter_map(Goal, List, Result) — map and filter in one pass. Keep the output value when Goal(Elem, Out) succeeds; skip elements where it fails.

safe_sqrt(X, Y) <- (X >= 0, eval_(X ** 0.5, Y))

test("filtermap") <- filter_map(safe_sqrt, [4, -1, 9, -2, 16], [2.0, 3.0, 4.0])

Folding

foldl/4, foldl/5, foldl/6

foldl(Goal, List, V0, V) — left fold. Applies Goal(Elem, Acc, NewAcc) across the list, threading an accumulator from V0 to V. foldl(Goal, Xs, Ys, V0, V) calls Goal(X, Y, Acc, NewAcc) over two lists of one length, and foldl/6 over three, as Scryer's library(lists).

Like maplist, foldl backtracks into every call (each solution of the goal gives another fold), and an open list enumerates: foldl(G, L, 0, S) with L unbound answers L = [] first, then longer lists.

add_step(X, ACC, OUT) <- (OUT == ACC + X)

test("sum") <- (
    foldl(add_step, [1, 2, 3, 4], 0, 10)
)

With an inline lambda:

test("sum inline") <- (
    foldl(((X, ACC, OUT) <- (OUT == ACC + X)), [1, 2, 3, 4], 0, 10)
)

Prefix & Suffix

take_while/3

take_while(Goal, List, Prefix) — longest prefix where Goal(Elem) succeeds.

test("takewhile") <- take_while((X <- (X < 5)), [1, 3, 7, 2, 4], [1, 3])

drop_while/3

drop_while(Goal, List, Suffix) — drop the prefix where Goal(Elem) succeeds.

test("dropwhile") <- drop_while((X <- (X < 5)), [1, 3, 7, 2, 4], [7, 2, 4])

span/4

span(Goal, List, Yes, No) — take_while + drop_while in one pass.

test("span") <- (
    span((X <- (X < 5)), [1, 3, 7, 2, 4], [1, 3], [7, 2, 4])
)

Grouping & Sorting

group_by/3

group_by(Goal, List, Groups) — group consecutive elements by key. Goal(Elem, Key) extracts the grouping key.

first_char(S, C) <- (C is ++S[0])

test("group by first char") <- (
    group_by(first_char, ['apple', 'avocado', 'banana', 'blueberry', 'cherry'], [['apple', 'avocado'], ['banana', 'blueberry'], ['cherry']])
)

Note: groups are consecutive runs, not global grouping. sort first if you need global groups.

sort_by/3

sort_by(Goal, List, Sorted) — sort by projected key.

abs_key(X, K) <- (abs_(X, K))

test("sort by abs") <- (
    sort_by(abs_key, [3, -1, -4, 2], [-1, 2, 3, -4])
)

max_by/3, min_by/3

max_by(Goal, List, max_) / min_by(Goal, List, min_) — element with the largest/smallest projected key.

str_len(S, K) <- (K is ++len(S))    # see [Python interop](python_integration.md)

test("longest") <- (
    max_by(str_len, ['hi', 'hello', 'hey'], 'hello')
)

Patterns & Recipes

Map then filter (pipeline)

sq(X, Y) <- (Y == X * X)

test("pipeline") <- (
    maplist(sq, [1, 2, 3, 4, 5], SQUARES),
    include((X <- (X > 10)), SQUARES, BIG),
    BIG == [16, 25]
)

flatten via foldl

test("flat") <- (
    foldl(((CHUNK, ACC, OUT) <- append(ACC, CHUNK, OUT)),
        [[1, 2], [3], [4, 5]],
        [],
        [1, 2, 3, 4, 5])
)

Count occurrences

count(PRED, LIST, N) <- (
    include(PRED, LIST, MATCHED),
    length(MATCHED, N)
)

test("count evens") <- count((X <- (X % 2 == 0)), [1, 2, 3, 4, 5, 6], 3)

Gotchas

  • Goal argument order matters — maplist/3 calls Goal(X, Y) where X is input, Y is output. foldl/4 calls Goal(Elem, AccIn, AccOut).
  • ^ is not a goal — Y^Goal means something only as bagof/setof's goal argument; call(Y^p(Y)) is existence_error(procedure, (^)/2), as in ISO and Scryer.
  • group_by groups consecutive runs — not global grouping. sort first if needed.
  • include/exclude are committed-choice — they test each element once (first solution only) and do not backtrack into the goal. maplist does.
  • Lambda syntax — single-arg: (X <- (body)). Multi-arg: ((X, Y) <- (body)) with extra outer parens for the tuple. See Lambdas for details.
  • call/N appends arguments — call(foo, 1, 2) calls foo(1, 2).

See also: Lambdas — goal closure syntax, Meta-Predicates — findall, bagof, setof, Lists — list operations.