DeepAI
Log In Sign Up

Synthesizing Set Functions

Set functions are a feature of functional logic programming to encapsulate all results of a non-deterministic computation in a single data structure. Given a function f of a functional logic program written in Curry, we describe a technique to synthesize the definition of the set function of f. The definition produced by our technique is based on standard Curry constructs. Our approach is interesting for three reasons. It allows reasoning about set functions, it offers an implementation of set functions which can be added to any Curry system, and it has the potential of changing our thinking about the implementation of non-determinism, a notoriously difficult problem.

READ FULL TEXT VIEW PDF

page 1

page 2

page 3

page 4

08/27/2020

Memoized Pull-Tabbing for Functional Logic Programming

Pull-tabbing is an evaluation technique for functional logic programs wh...
09/22/2020

Research Summary on Implementing Functional Patterns by Synthesizing Inverse Functions

In this research summary we present our recent work on implementing func...
02/15/2021

On synthesizing Skolem functions for first order logic formulae

Skolem functions play a central role in logic, from eliminating quantifi...
03/12/2021

Reasoning about the garden of forking paths

Lazy evaluation is a powerful tool for functional programmers. It enable...
02/14/2022

Quantitative Strongest Post

We present a novel strongest-postcondition-style calculus for quantitati...
02/14/2020

Functional Programming in Pattern-Match-Oriented Programming Style

Throughout the history of functional programming, recursion has emerged ...

1 Introduction

Functional logic languages, such as Curry and TOY, combine the most important features of functional and logic languages. In particular, the combination of lazy evaluation and non-determinism leads to better evaluation strategies compared to logic programming [2] and new design patterns [3]. However, the combination of these features poses new challenges. In particular, the encapsulation of non-strict non-deterministic computations has not a universally accepted solution so that different Curry systems offer different implementations for it. Encapsulating non-deterministic computations is an important feature for application programming when the task is to show whether some problem has a solution or to compare different solutions in order to compute the best one. For this purpose, let denote the set of all the values of an expression . The problem with such an encapsulation operator is the fact that the might share subexpressions which are defined outside . For instance, consider the expression

let x = 0?1 in (1)

The infix operator “?” denotes a non-deterministic choice, i.e., the expression “0?1” has two values: 0 or 1. Since the non-determinism of x is introduced outside , the question arises whether this should be encapsulated. Strong encapsulation, which is similar to Prolog’s findall, requires to encapsulate all non-determinism occurring during the evaluation of the encapsulated expression. In this case, expression (1) evaluates to the set . As discussed in [8], a disadvantage of strong encapsulation is its dependence on the evaluation strategy. For instance, consider the expression

let x = 0?1 in (, x) (2)

If the tuple is evaluated from left to right, the first component evaluates to but the second component non-deterministically evaluates to the values 0 and 1 so that the expression (1) evaluates to the values (,0) and (,1). However, in a right-to-left evaluation of the tuple, x is evaluated first to one of the values 0 and 1 so that, due to sharing, the expression (1) evaluates to the values (,0) and (,1).

To avoid this dependency on the evaluation strategy, weak encapsulation of only encapsulates the non-determinism of but not the non-determinism originating from expressions created outside . Thus, weak encapsulation produces the last result of (1) independent of the evaluation strategy. Weak encapsulation has the disadvantage that its meaning depends on the syntactic structure of expressions. For instance, the expressions “let x = 0?1 in ” and “” have different values. To avoid misunderstandings and make the syntactic structure of encapsulation explicit, set functions have been proposed [5]. For any function , there is a set function which computes the set of all the values for given argument values. The set function encapsulates the non-determinism caused by the definition of but not non-determinism originating from arguments. For instance, consider the operation

double x = x + x (3)

The result of double is always a set with a single element since the definition of double does not contain any non-determinism. Thus, double (0?1) evaluates to the two sets and .

Although set functions fit well into the framework of functional logic programming, their implementation is challenging. For instance, the Curry system PAKCS [15] compiles Curry programs into Prolog programs so that non-determinism is implemented for free. Set functions are implemented in PAKCS by Prolog’s findall. To obtain the correct separation of non-determinism caused by arguments and functions, as discussed above, arguments are completely evaluated before the findall encapsulation is invoked. Although this works in many cases, there are some situations where this implementation does not deliver any result. For instance, if the complete evaluation of arguments fails or does not terminate, no result is computed even if the set function does not demand the complete argument values. Furthermore, if the set is infinite, findall does not terminate even if the goal is only testing whether the set is empty. Thus, the PAKCS implementation of set functions is “too strict.”

These problems are avoided by the implementation of set functions in the Curry system KiCS2, which compiles Curry programs into Haskell programs and represents non-deterministic values in tree-like structures [9]. A similar but slightly different representation is used to implement set functions. Due to the interaction of different levels of non-determinism, the detailed implementation is quite complex so that a simpler implementation is desirable.

In this paper, we propose a new implementation of set functions that can be added to any Curry system. It avoids the disadvantages of existing implementations by synthesizing an explicit definition of a set function for a given function. Depending on the source code of a function, simple or more complex definitions of a set function are derived. For instance, nested set functions require a more complex scheme than top-level set functions, and functions with non-linear right-hand sides require an explicit implementation of the call-time choice semantics.

The paper is structured as follows. In the next section, we review some aspects of functional logic programming and Curry. After the definition of set functions in Sect. 3, we introduce in Sect. 4 plural functions as an intermediate step towards the synthesis of set functions. A first and simple approach to synthesize set functions is presented in Sect. 5 before we discuss in Sect. 6 and 7 the extension to non-linear rules and nested set functions, respectively. Sect. 8 discussed related work before we conclude in Sect. 9.

2 Functional Logic Programming and Curry

We assume familiarity with the basic concepts of functional logic programming [6, 14] and Curry [16]. Therefore, we briefly review only those aspects that are relevant for this paper.

Although Curry has a syntax close to Haskell [21], there is an important difference in the interpretation of rules defining an operation. If there are different rules that might be applicable to reduce an expression, all rules are applied in a non-deterministic manner. Hence, operations might yield more than one result on a given input. Non-deterministic operations, which are interpreted as mappings from values into sets of values [13], are an important feature of contemporary functional logic languages. The archetype of non-deterministic operations is the choice operator “?” defined by

_ ? y = y

Typically, this operator is used to define other non-deterministic operations like

Thus, the expression coin evaluates to one of the values 0 or 1. Non-deterministic operations are quite expressive since they can be used to completely eliminate logic variables in functional logic programs, as show in [4, 11]. Therefore, we ignore them in the formal development below. For instance, a Boolean logic variable can be replaced by the non-deterministic generator operation for Booleans defined by

aBool = False ? True (4)

Passing non-deterministic operations as arguments, as in the expression doublecoin, might cause a semantical ambiguity. If the argument coin is evaluated before calling double, the expression has two values, 0 and 2. However, if the argument coin is passed unevaluated to the right-hand side of double and, thus, duplicated, the expression has three different values: 0, 1, or 2. These two interpretations are called call-time choice and run-time choice [18]. Contemporary functional logic languages stick to the call-time choice, since this leads to results which are independent of the evaluation strategy and has the rewriting logic CRWL [13] as a logical foundation for declarative programming with non-strict and non-deterministic operations. Furthermore, it can be implemented by sharing which is already available in implementations of non-strict languages. In this paper, we use a simple reduction relation, equivalent to CRWL, that we sketch without giving all details (which can be found in [19]).

A value is an expression without defined operations. To cover non-strict computations, expressions can also contain the special symbol to represent undefined or unevaluated values. A partial value is a value that might contain occurrences of . A partial constructor substitution is a substitution that replaces variables by partial values. A context is an expression with some “hole.” Then expressions are reduced according to the following reduction relation:
where is a program rule and a partial constructor substitution where
The first rule models the call-time choice: if a rule is applied, the actual arguments of the operation must have been evaluated to partial values. The second rule models non-strictness by allowing the evaluation of any subexpression to an undefined value (which is intended if the value of this subexpression is not demanded). As usual, denotes the reflexive and transitive closure of this reduction relation. We also write instead of if is a (partial) value.

For the sake of simplicity, we assume that programs are already translated into a simple standard form: conditional rules are replaced by if-then-else expressions and the left-hand sides of all operations (except for “?”) are uniform [20] i.e., either the operation is defined by a single rule where all arguments are distinct variables, or in the left-hand sides of all rules only the last (or any other fixed) argument is a constructor with variables as arguments where each constructor of a data type occurs in exactly one rule. In particular, rules are not overlapping so that non-determinism is represented by ?-expressions.

3 Set Functions

Set functions have been introduced in Sect. 1. Their main feature is to encapsulate only the non-determinism caused by the definition of the corresponding function. Similarly to non-determinism, set functions encapsulate failures only if they are caused by the function’s definition. If failed denotes a failing computation, i.e., an expression without a value, the expression doublefailed has no value (and not the empty set as a value). Since the meaning of failures and nested set functions has not been discussed in [5], Christiansen et al. [10] propose a rigorous denotational semantics for set functions. In order to handle failures and choices in nested applications of set functions, computations are attached with “nesting levels” so that failures caused by different encapsulation levels can be distinguished.

In the following, we use a simpler model of set functions. Formally, set functions return sets. However, for simplicity, our implementation returns multisets, instead of sets, represented as some abstract data type. Converting multisets into sets is straightforward for the representation we choose. If is a type, denotes the type of a set of elements of type . The meaning of a set function can be defined as follows:

Definition 1

Given a unary (for simplicity) function , the set function of , , is defined as follows: For every partial value of type , value of type , and set of elements of type , iff and .

This definition accommodates the different aspects of set functions discussed in the introduction. If the evaluation of an expression leads to a failure or choice but its value is not required by the set function, it does not influence the result of the set function since can be derived to the partial value .

Example 1

Consider the following function:

ndconst x y = x ? 1 (5)

The value of ndconst2failed is , and ndconst(2?4)failed has the values and .

Given a function , we want to develop a method to synthesize the definition . A difficulty is that might be composed of other functions whose non-determinism should also be encapsulated by . This can be solved by introducing plural functions, which are described next.

4 Plural Functions

If and are functions, their composition is well defined by for all of type . However, the corresponding set functions, and , are not composable because their types mismatch—an output of cannot be an input of . To support the composition of functions that return sets, we need functions that take sets as arguments.111The notion of “plural function” is also used in [22] to define a “plural” semantics for functional logic programs. Although the type of their plural functions is identical to ours, their semantics is quite different.

Definition 2

Let be a function. We call plural function of any function with the following property: for all and such that , (1) if then there exists some such that and (2) if and , then .

The above definition generalizes to functions with more than one argument. The following display shows both the type and an example of application of the plural function, denoted by “++”, of the usual list concatenation “++”:

(++) :: {[a]} -> {[a]} -> {[a]}
{[1],[2]} ++ {[],[3]} = {[1],[1,3],[2],[2,3]}
(6)

Plural functions are unique, composable, and cover all the results of set functions (see appendix for details). Since plural functions are an important step towards the synthesis of set functions, we discuss their synthesis first. To implement plural functions in Curry, we have to decide how to represent sets (our implementation returns multisets) of elements. An obvious representation are lists. Since we will later consider non-linear rules and also nested set functions where non-determinism of different encapsulation levels are combined, we use search trees [8] to represent choices between values. The type of a search tree parameterized over the type of elements can be defined as follows:

Hence, a search tree is either an expression in head-normal form, a failure, or a choice between search trees. Although this definition does not enforce that the argument of a Val constructor is in head-normal form, this invariant will be ensured by our synthesis method for set functions, as presented below. For instance, the plural function of the operation aBool (2) can be defined as

aBoolP = Choice (Val False) (Val True)

The plural function of the logical negation not defined by

not False = True
not True  = False
(7)

takes a search tree as an argument so that its definition must match all search tree constructors. Since the matching structure is similar for all operations performing pattern matching on an argument, we use the following generic operation to apply an operation defined by pattern matching to a non-deterministic argument:

222Actually, this operation is the monadic “bind” operation with flipped arguments if ST is an instance of MonadPlus, as proposed in [12]. Here, we prefer to provide a more direct implementation.

applyST f (Val x)        = f x
applyST _ Fail           = Fail
applyST f (Choice x1 x2) = Choice (f applyST x1) (f applyST x2)

Hence, failures remain as failures, and a choice in the argument leads to a choice in the result of the operation, which is also called a pull-tab step [1]. Now the plural function of not can be defined by (shortly we will specify a systematic translation method)

notP :: ST Bool -> ST Bool
notP = applyST $ \x -> case x of False -> Val True
                                  True  -> Val False

The synthesis of plural functions for uniform programs is straightforward: pattern matching is implemented with applyST and function composition in right-hand sides comes for free. For instance, the plural function of

is

So far we considered only base values in search trees. If one wants to deal with structured data, like lists of integers, a representation like ST [Int] is not appropriate since non-determinism can occur in any constructor of the list, as shown by

The expression one23 evaluates to [1], [2], [1,3], and [2,3]. If we select only the head of the list, the non-determinism in the tail does not show up, i.e., headone23 evaluates to two values 1 and 2. This demands for a representation of head-normal forms with possible search tree arguments. The head-normal forms of non-deterministic lists are the usual list constructors where the cons arguments are search trees:

The plural representation of one23 is

one23P = Val (Cons (Choice (Val 1) (Val 2))
                   (Choice (Val Nil) (Val (Cons (Val 3) (Val Nil)))))

The plural function of head is synthesized as

headP :: ST (STList a) -> ST a
headP = applyST $ \xs -> case xs of Nil      -> Fail
                                     Cons x _ -> x

so that headPone23P evaluates to Choice(Val1) (Val2), as intended.

To provide a precise definition of this transformation, we assume that all operations in the program are uniform (see Sect. 2). The plural transformation of these kinds of function definitions is defined as follows (where denotes the constructor of the non-deterministic type, like STList, corresponding to the original constructor ):

Note that and have different types in the original and transformed program, e.g., an argument of type Int is transformed into an argument of type STInt. Furthermore, expressions occurring in the function bodies are transformed according to the following rules:

Fail

The presented synthesis of plural functions is simple and yields compositionality and laziness. Thus, they are a good basis to define set functions, as shown next.

5 Synthesis of Set Functions: The Simple Way

Plural functions take sets as arguments whereas set functions are applied to standard expressions which might not be evaluated. To distinguish these possibly unevaluated arguments from head-normal forms, we add a new constructor to search trees

and extend the definition of applyST with the rule

Furthermore, plural functions yield non-deterministic structures which might not be completely evaluated. By contrast, set functions yield sets of values, i.e., completely evaluated elements. In order to turn a plural function into a set function, we have to evaluate the search tree structure into the set of their values. For the sake of simplicity, we represent the latter as ordinary lists. Thus, we need an operation like

to extract all the values from a search tree. For instance, the expression

should evaluate to the list [1,2]. This demands for the evaluation of all the values in a search tree (which might be head-normal forms with choices at argument positions) into its complete normal form. We define a type class333Although the current definition of Curry [16] does not include type classes, many implementations of Curry, like PAKCS, KiCS2, or MCC, support them. for this purpose:

  nf :: a -> ST a$\listline$

Each instance of this type class must define a method nf which evaluates a given head-normal form into a search tree where all Val arguments are completely evaluated. Instances for base types are easily defined:

  nf x = Val x

The operation nf is easily extended to arbitrary search trees:444The use of seq ensures that the Uneval argument is evaluated. Thus, non-determinism and failures in arguments of set functions are not encapsulated, as intended.

nfST (Val x)        = nf x
nfST (Uneval x)     = x seq nf x
nfST Fail           = Fail
nfST (Choice x1 x2) = Choice (nfST x1) (nfST x2)

Now we can define an operation that collects all the values in a search tree (without Uneval constructors) into a list by a depth-first strategy:

searchDFS (Val x)        = [x]
searchDFS Fail           = []
searchDFS (Choice x1 x2) = searchDFS x1 ++ searchDFS x2

Thus, failures are ignored and choices are concatenated. Combining these two operations yields the desired definition of stValues:

stValues = searchDFS . nfST

NF instances for structured types can be defined by moving choices and failures in arguments to the root:

  nf Nil         = Val Nil
  nf (Cons x xs) = case nfST x of
    Choice c1 c2 -> Choice (nf (Cons c1 xs)) (nf (Cons c2 xs))
    Fail         -> Fail
    y            -> case nfST xs of
      Choice c1 c2 -> Choice (nf (Cons y c1)) (nf (Cons y c2))
      Fail         -> Fail
      ys           -> Val (Cons y ys)

For instance, the non-deterministic list value [1?2] can be described by the ST structure

so that stValuesnd01 moves the inner choice to the top-level and yields the list

which represents the set .

As an example for our first approach to synthesize set functions, consider the following operation (from Curry’s prelude) which non-deterministically returns any element of a list:

anyOf (x:xs) = x ? anyOf xs

Since set functions do not encapsulate non-determinism caused by arguments, the expression anyOf[0?1,2,3] evaluates to the sets and .

In order to synthesize the set function for anyOf by exploiting plural functions, we have to convert ordinary types, like [Int], into search tree types, like ST (STList Int), and vice versa. For this purpose, we define two conversion operations for each type and collect their general form in the following type class:555Multi-parameter type classes are not yet supported in the Curry systems PAKCS and KiCS2. The code presented here is more elegant, but equivalent, to the actual implementation.

  toValST   :: a -> b
  fromValST :: b -> a

Instances for base and list types are easily defined:

  toValST   = id
  fromValST = id$\listline$
instance ConvertST a b => ConvertST [a] (STList b) where
  toValST []     = Nil
  toValST (x:xs) = Cons (toST x) (toST xs)
  fromValST Nil                     = []
  fromValST (Cons (Val x) (Val xs)) = fromValST x : fromValST xs

where the operation toST is like toValST but adds a Uneval constructor:

toST = Uneval . toValST

The (informal) precondition of fromValST is that its argument is already fully evaluated, e.g., by an operation like stValues. Therefore, we define the following operation to translate an arbitrary search tree into the list of its Curry values:

fromST = map fromValST . stValues

As already mentioned, we use lists to represent multisets of values:

However, one could also use another (abstract) data type to represent multisets or even convert them into sets, if desired.

Now we have all parts to synthesize a set function: convert an ordinary value into its search tree representation, apply the plural function on it, and translate the search tree back into the multiset (list) of the values contained in this tree. We demonstrate this by synthesizing the set function of anyOf.

The uniform representation of anyOf performs complete pattern matching on all constructors:

anyOf (x:xs) = x ? anyOf xs

We easily synthesize its plural function according to the scheme of Sect. 4:

anyOfP :: ST (STList Int) -> ST Int
anyOfP = applyST $ \xs ->
           case xs of Nil       -> Fail
                      Cons x xs -> Choice x (anyOfP xs)

Finally, we obtain its set function by converting the argument into the search tree and the result of the plural function into a multiset of integers:

anyOfS = fromST . anyOfP . toST

The behavior of our synthesized set function is identical to their original definition, e.g., anyOfS[0?1,2,3] evaluates to the lists [0,2,3] and [1,2,3], i.e., non-determinism caused by arguments is not encapsulated. This is due to the fact that the evaluation of arguments, if they are demanded inside the set function, are initiated by standard pattern matching so that a potential non-deterministic evaluation leads to a non-deterministic evaluation of the synthesized set function.

In contrast to the strict evaluation of set functions in PAKCS, as discussed in the introduction, our synthesized set functions evaluate their arguments lazily. For instance, the set function of ndconst defined in Example 1 is synthesized as follows:

ndconstP nx ny = Choice nx (Val 1)
ndconstS :: Int -> Int -> Values Int
ndconstS x y = fromST (ndconstP (toST x) (toST y))

Since the second argument of ndconstS is never evaluated, the expression ndconstS2failed evaluates to [2,1] and ndconstS(2?4)(3?5) yields the lists [2,1] and [4,1]. The set function implementation of PAKCS fails on the first expression and yields four results on the second one. Hence, our synthesized set function yields better results than PAKCS, in the sense that it is more complete and avoids duplicated results. Morever, specific primitive operations, like findall, are not required.

The latter property is also interesting from another point of view. Since PAKCS uses Prolog’s findall, the evaluation strategy is fixed to a depth-first search strategy implemented by backtracking. Our implementation allows more flexible search strategies by modifying the implementation of stValues. Actually, one can generalize search trees and stValues to a monadic structure, as done in [7, 12], to implement various strategies for non-deterministic programming.

A weak point of our current synthesis is the handling of failures. For instance, the evaluation of anyOfS[failed,1] fails (due to the evaluation of the first list element) whereas according to Def. 1. To correct this incompleteness, failures resulting from argument evaluations must be combined with result sets. This can be done by extending search trees and distinguishing different sources of failures, but we omit it here since a comprehensive solution to this issue will be presented in Sect. 7 when nested applications of set functions are discussed.

6 Adding Call-Time Choice

We have seen in Sect. 1 that the expression double(0?1) should evaluate to the values 0 or 2 due to the call-time choice semantics. Thus, the set function of

double01 = double (0?1)

should yield the multiset . However, with the current synthesis, the corresponding set function yields the list [0,1,1,2] and, thus, implements the run-time choice. The problem arises from the fact that the non-determinstic choice in the synthesized plural function

double01P = doubleP (Choice (Val 0) (Val 1))

is duplicated by doubleP. In order to implement the call-time choice, the same decision (left or right choice) for both duplicates has to be made. Instead, the search operation searchDFS handles these choices independently and is unaware of the duplication.

To tackle this problem, we follow the idea implemented in KiCS2 [9] and extend our search tree structure by identifiers for choices (represented by the type ID) as follows:

The changes to previously introduced operations on search trees, like applyST or nfST, are minimal and straightforward as we only have to keep a choice’s identifier in their definitions. The most significant change occurs in the search operation. As shown in [9], the call-time choice can be implemented by storing the decision for a choice, when it is made for the first time, during the traversal of the search tree and looking it up later when encountering the same choice again. We introduce the type

for decisions and use an association list666Of course, one can replace such lists by more efficient access structures. as an additional argument to the search operation to store such decisions. The adjusted depth-first search then looks as follows:

searchDFS _ (Val x)          = [x]
searchDFS _ Fail             = []
searchDFS m (Choice i x1 x2) = case lookup i m of
  Nothing    -> searchDFS ((i,Left):m) x1 ++
                  searchDFS ((i,Right):m) x2
  Just Left  -> searchDFS m x1
  Just Right -> searchDFS m x2

When extracting all the values from a search tree, we initially pass an empty list to the search operation since no decisions have been made at that point:

stValues = searchDFS [] . nfST

Finally, we have to ensure that the choices occurring in synthesized plural functions are provided with unique identifiers. To this end, we assume a type IDSupply that represents an infinite set of such identifiers along with the following operations:

uniqueID                :: IDSupply -> ID
leftSupply, rightSupply :: IDSupply -> IDSupply

The operation initSupply yields an initial identifier set. The operation uniqueID yields an identifier from such a set while the operations leftSupply and rightSupply both yield disjoint subsets without the identifier obtained by uniqueID (see [9] for a discussion about implementing these operations.). When synthesizing plural functions, we add an additional argument of type IDSupply and use the aforementioned operations on it to provide unique identifiers to every choice. The synthesized set function has to pass the initial identifier supply initSupply to the plural function. In the case of double01, it looks as follows:

double01P s = doubleP (leftSupply s)
                      (Choice (uniqueID s) (Val 0) (Val 1))
double01S :: Values Int
double01S = fromST (doubleP initSupply)

With this modified synthesis, the set function yields the expected result [0,2]. Note that this extended scheme is necessary only if some operation involved in the definition of the set function has rules with non-linear right-hand sides, i.e., might duplicate argument expressions. For the sake of readability, we omit this extension in the next section where we present another extension necessary when set functions are nested.

7 Synthesis of Nested Set Functions

So far we considered the synthesis of set functions that occur only at the top-level of functional computations, i.e., which are not nested inside other set functions. The synthesis was based on the translation of functions involved in the definition of a set function into plural functions and extracting all the values represented by a search tree into a list structure. If set functions are nested, the situation becomes more complicated since one has to define the plural function of an inner set function. Moreover, choices and failures produced by different set functions, i.e., levels of encapsulations, must be distinguished according to [10]. Although nested set functions are seldom used, a complete implementation of set functions must consider them. Therefore, we discuss in this section how we can extend the scheme proposed so far to accommodate nested set functions.

The original proposal of set functions [5] emphasized the idea to distinguish non-determinism of arguments from non-determinism of the function definition. However, the influence of failing computations and the combination of nested set functions was not specified. These aspects are discussed in [10] where a denotational semantics for functional logic programs with weak encapsulation is proposed. Roughly speaking, an encapsulation level is attached to failures and choices. These levels are taken into account when value sets are extracted from a nested non-determinism structure to ensure that failures and choices are encapsulated by the function they belong to and not any other. We can model this semantics by extending the structure of search trees as follows:

The additional argument of the constructors Fail and Choice specifies the encapsulation level.

Consider the definition

notf = failed (8)

and the expression notf. Although the right-hand side of notf fails because the argument of not is demanded w.r.t. the definition (4), the source of the failure is inside its definition so that the failure is encapsulated and the result of notf is the empty set. However, if we define

nots x = x (9)

and evaluate notsfailed, the computation fails since the failure comes from outside and is not encapsulated. These issues are discussed in [10] where it has been argued that failures outside encapsulated search should lead to a failure instead of an empty set only if there are no other results. For instance, the expression anyOffailed has no value (since the demanded argument is an outside failure) whereas the value of the expression

anyOf[failed,1] (10)

is the set with the single element 1. This semantics can be implemented by comparing the levels of failures occurring in search trees (see [10] for details).

With the extension of search trees introduced above, we are well prepared to implement this semantics in Curry itself except for one point: outside failures always lead to a failure of the complete evaluation if their value is needed in the encapsulated search. Thus, the evaluation of (7) will always fail. In order to avoid this, we have to transform such a failure into the search tree element Fail0 (where 0 is the “top” encapsulation level, i.e., outside any set function). For this purpose, we modify the definitions of applyST and nfST on arguments matching the Uneval constructor by checking whether the evaluation of the argument to a head-normal form fails:777This requires a specific primitive isFail to catch failures, which is usually supported in Curry implementations to handle exceptions.

nfST (Uneval x) = if isFail x then Fail 0 else x seq nf x

Then one can synthesize plural and set functions similarly to the already presented scheme. In order to set the correct encapsulation level in Fail and Choice constructors, every function has the current encapsulation level as an additional argument. Finally, one also has to synthesize plural functions of set functions if they are used inside other set functions. For instance, the set function of not has type

but the plural function of this set function must represent the result set again as a search tree, i.e., it has the type

(the first argument is the encapsulation level). To evaluate the search tree structure returned by such plural set functions, we need an operation which behaves similarly to stValues but returns a search tree representation of the list of values, i.e., this operation has the type

Note that this operation also takes the encapsulation level as its first argument. For instance, failures are only encapsulated (into an empty list) if they are on the same level, i.e., there is the following defining rule for stValuesP:

Choices are treated in a similar way where failures in different alternatives are merged to their maximum level according to the semantics of [10], e.g.,

evaluates to Val Nil (representing the empty set of values).

Now we can define notSP by evaluating the result of notP with stValuesP where the current encapsulation level is increased:

The plural function of notf (7) is straightforward (note that the level of the generated failure is the current encapsulation level):

notfP e = notSP e (Fail e)

The set function of notf is synthesized as presented before except that we additionally provide 1 as the initial encapsulation level (this is also the level encapsulated by fromST):

notfS = fromST (notfP 1)

As we have seen, nested set functions can be synthesized with a scheme similar to simple set functions. In order to correctly model the semantics of [10], an encapsulation level is added to each translated operation which is used to generate the correct Fail and Choice constructors. In order integrate the synthesized set functions into standard Curry programs, arguments passed to synthesized set functions must be checked for failures when their values are demanded.

The extensions presented in the previous and this section can be combined without problems. Concrete examples for this combination and more examples for the synthesis techniques presented in this paper are available on-line.888https://github.com/finnteegen/synthesizing-set-functions In particular, there are also examples for the synthesis of higher-order functions, which we omitted in this paper due to the additional complexity of synthesizing the plural functions of higher-order arguments.

8 Related Work

The problems caused by integrating encapsulated search in functional logic programs are discussed in [8] where the concepts of strong and weak encapsulation are distinguished. Weak encapsulation fits better to declarative programming since the results do not depend on the order of evaluation. Set functions [5] makes the boundaries between different sources of non-determinism clear. The semantical difficulties caused by nesting set functions are discussed in [10] where a denotational semantics for set functions is presented.

The implementation of backtracking and non-determinism in functional languages has a long tradition [24]. While earlier approaches concentrated on embedding Prolog-like constructs in functional languages (e.g., [17, 23]), the implementation of demand-driven non-determinism, which is the core of contemporary functional logic languages [2], has been less explored. A monadic implementation of the call-time choice is developed in [12] which is the basis to translate a subset of Curry to Haskell [7]. Due to performance problems with this generic approach, KiCS2, another compiler from Curry to Haskell, is proposed in [9]. Currently, KiCS2 is the only system implementing encapsulated search and set functions according to [10], but the detailed implementation is complex and, thus, difficult to maintain. This fact partially motivated the development of the approach described in this paper.

9 Conclusions

We have presented a technique to synthesize the definition of a set function of any function defined in a Curry program. This is useful to add set functions and encapsulated search to any Curry system so that an explicit handling of set functions in the run-time system is not necessary. Thanks to our method, one can add a better (i.e., less strict) implementation of set functions to the Prolog-based Curry implementation PAKCS or simplify the run-time system of the Haskell-based Curry implementation KiCS2.

A disadvantage of our approach is that it increases the size of the transformed program due to the addition of the synthesized code. Considering the fact that the majority of application code is deterministic and not involved in set functions, the increased code size is acceptable. Nevertheless, it is an interesting topic for future work to evaluate this for application programs and try to find better synthesis principles (e.g., for specific classes of operations) which produces less additional code.

Our work has the potential of both immediate and far reaching paybacks. We offer a set-based definition of set functions simpler and more immediate than previous ones. We offer a notion of plural function that is original and natural. We show interesting relationships between the two that allow us to better understand, reason about and compute with these concepts. The immediate consequence is an implementation of set functions competitive with previous proposals.

A more intriguing aspect of our work is the possibility of replacing any non-deterministic function, , in a program with its set function, which is deterministic, by enumerating all the results of . Thus, a (often non-deterministic) functional logic program, would become a deterministic program. A consequence of this change is that the techniques for the implementation of non-determinism, such as backtracking, bubbling and pull-tabbing, which are the output of a tremendous intellectual effort of the last few decades, would become unnecessary.

References

  • [1] A. Alqaddoumi, S. Antoy, S. Fischer, and F. Reck. The pull-tab transformation. In Proc. of the Third International Workshop on Graph Computation Models, pages 127–132. Enschede, The Netherlands, 2010. Available at http://gcm-events.org/gcm2010/pages/gcm2010-preproceedings.pdf.
  • [2] S. Antoy, R. Echahed, and M. Hanus. A needed narrowing strategy. Journal of the ACM, 47(4):776–822, 2000.
  • [3] S. Antoy and M. Hanus. Functional logic design patterns. In Proc. of the 6th International Symposium on Functional and Logic Programming (FLOPS 2002), pages 67–87. Springer LNCS 2441, 2002.
  • [4] S. Antoy and M. Hanus. Overlapping rules and logic variables in functional logic programs. In Proceedings of the 22nd International Conference on Logic Programming (ICLP 2006), pages 87–101. Springer LNCS 4079, 2006.
  • [5] S. Antoy and M. Hanus. Set functions for functional logic programming. In Proceedings of the 11th ACM SIGPLAN International Conference on Principles and Practice of Declarative Programming (PPDP’09), pages 73–82. ACM Press, 2009.
  • [6] S. Antoy and M. Hanus. Functional logic programming. Communications of the ACM, 53(4):74–85, 2010.
  • [7] B. Braßel, S. Fischer, M. Hanus, and F. Reck. Transforming functional logic programs into monadic functional programs. In Proc. of the 19th International Workshop on Functional and (Constraint) Logic Programming (WFLP 2010), pages 30–47. Springer LNCS 6559, 2011.
  • [8] B. Braßel, M. Hanus, and F. Huch. Encapsulating non-determinism in functional logic computations. Journal of Functional and Logic Programming, 2004(6), 2004.
  • [9] B. Braßel, M. Hanus, B. Peemöller, and F. Reck. KiCS2: A new compiler from Curry to Haskell. In Proc. of the 20th International Workshop on Functional and (Constraint) Logic Programming (WFLP 2011), pages 1–18. Springer LNCS 6816, 2011.
  • [10] J. Christiansen, M. Hanus, F. Reck, and D. Seidel. A semantics for weakly encapsulated search in functional logic programs. In Proc. of the 15th International Symposium on Principle and Practice of Declarative Programming (PPDP’13), pages 49–60. ACM Press, 2013.
  • [11] J. de Dios Castro and F.J. López-Fraguas. Extra variables can be eliminated from functional logic programs. Electronic Notes in Theoretical Computer Science, 188:3–19, 2007.
  • [12] S. Fischer, O. Kiselyov, and C. Shan. Purely functional lazy nondeterministic programming. Journal of Functional programming, 21(4&5):413–465, 2011.
  • [13] J.C. González-Moreno, M.T. Hortalá-González, F.J. López-Fraguas, and M. Rodríguez-Artalejo. An approach to declarative programming based on a rewriting logic. Journal of Logic Programming, 40:47–87, 1999.
  • [14] M. Hanus. Functional logic programming: From theory to Curry. In Programming Logics - Essays in Memory of Harald Ganzinger, pages 123–168. Springer LNCS 7797, 2013.
  • [15] M. Hanus, S. Antoy, B. Braßel, M. Engelke, K. Höppner, J. Koj, P. Niederau, R. Sadre, and F. Steiner. PAKCS: The Portland Aachen Kiel Curry System. Available at http://www.informatik.uni-kiel.de/~pakcs/, 2017.
  • [16] M. Hanus (ed.). Curry: An integrated functional logic language (vers. 0.9.0). Available at http://www.curry-language.org, 2016.
  • [17] R. Hinze. Prolog’s control constructs in a functional setting - axioms and implementation. International Journal of Foundations of Computer Science, 12(2):125–170, 2001.
  • [18] H. Hussmann. Nondeterministic algebraic specifications and nonconfluent term rewriting. Journal of Logic Programming, 12:237–255, 1992.
  • [19] F.J. López-Fraguas, J. Rodríguez-Hortalá, and J. Sánchez-Hernández. A simple rewrite notion for call-time choice semantics. In Proceedings of the 9th ACM SIGPLAN International Conference on Principles and Practice of Declarative Programming (PPDP’07), pages 197–208. ACM Press, 2007.
  • [20] J.J. Moreno-Navarro, H. Kuchen, R. Loogen, and M. Rodríguez-Artalejo. Lazy narrowing in a graph machine. In Proc. Second International Conference on Algebraic and Logic Programming, pages 298–317. Springer LNCS 463, 1990.
  • [21] S. Peyton Jones, editor. Haskell 98 Language and Libraries—The Revised Report. Cambridge University Press, 2003.
  • [22] A. Riesco and J. Rodríguez-Hortalá. Singular and plural functions for functional logic programming. Theory and Practice of Logic Programming, 14(1):65–116, 2014.
  • [23] S. Seres, M. Spivey, and T. Hoare. Algebra of logic programming. In Proc. ICLP’99, pages 184–199. MIT Press, 1999.
  • [24] P. Wadler. How to replace failure by a list of successes. In Functional Programming and Computer Architecture, pages 113–128. Springer LNCS 201, 1985.

Appendix 0.A Properties of Plural Functions

This section contains some interesting properties of plural functions.

Lemma 1

The plural function of a function is unique.

Proof

Suppose that both and are plural functions of some function . Let be any set such that and , for some and . We show . For any , by Def. 2, point (1), applied to , there exists some in such that . Since and is a plural function of , Def. 2, point (2), implies that is in . By symmetry, . Hence, .

Lemma 2

If and are composable functions, then .

Proof

First, we prove that for any , . Suppose for some sets and . There exists a set such that and . If is some element of , then there exists some in such that is a value of , and there exists some in such that is a value . Consequently is a value of and is an element of . The proof that for any , is similar.

The following claim establishes key relationships between the set and the plural functions of a function.

Theorem 0.A.1

For any function , argument of , and argument of :

  1. and

  2. .

Proof

We prove that . If , then, by Def. 1, is a value of , then, by Def. 2, is an element of . The proof that is similar. Hence condition (1) holds.
We now prove that . For any , if , by Def. 2, there exists some such that is a value of . By Def. 1, . The proof that is similar. Hence condition (2) holds.