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 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 predicate instance.


Mapping

maplist/2

maplist(Goal, List) — test Goal(Elem) for every element. Succeeds if the goal succeeds for all elements.

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).

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, Y == X ** 0.5)

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

Folding

foldl/4

foldl(Goal, List, V0, V) — left fold. Applies Goal(Elem, Acc, NewAcc) across the list, threading an accumulator from V0 to V.

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 := ++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 := ++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 mattersmaplist/3 calls Goal(X, Y) where X is input, Y is output. foldl/4 calls Goal(Elem, AccIn, AccOut).
  • group_by groups consecutive runs — not global grouping. sort first if needed.
  • Lambdas are committed-choiceinclude tests each element once (first solution only). It does not backtrack into the goal.
  • 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 argumentsCall(foo, 1, 2) calls foo(1, 2).

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