Flix (programming language)
Flix is a functional, imperative, and logic programming language developed at Aarhus University, with funding from the Independent Research Fund Denmark,[2] and by a community of open source contributors.[3] The Flix language supports algebraic data types, pattern matching, parametric polymorphism, currying, higher-order functions, extensible records,[4] channel and process-based concurrency, and tail call elimination. Two notable features of Flix are its type and effect system[5] and its support for first-class Datalog constraints.[6] The Flix type and effect system supports Hindley-Milner-style type inference. The system separates pure and impure code: if an expression is typed as pure then it cannot produce an effect at run-time. Higher-order functions can enforce that they are given pure (or impure) function arguments. The type and effect system supports effect polymorphism[7][8] which means that the effect of a higher-order function may depend on the effect(s) of its argument(s). Flix supports Datalog programs as first-class values. A Datalog program value, i.e. a collection of Datalog facts and rules, can be passed to and returned from functions, stored in data structures, and composed with other Datalog program values. The minimal model of a Datalog program value can be computed and is itself a Datalog program value. In this way, Flix can be viewed as a meta programming language for Datalog. Flix supports stratified negation and the Flix compiler ensures stratification at compile-time.[9] Flix also supports an enriched form of Datalog constraints where predicates are given lattice semantics.[10][11][12][13] OverviewFlix is a programming language in the ML-family of languages. Its type and effect system is based on Hindley-Milner with several extensions, including row polymorphism and Boolean unification. The syntax of Flix is inspired by Scala and uses short keywords and curly braces. Flix supports uniform function call syntax which allows a function call While many programming languages support a mixture of functional and imperative programming, the Flix type and effect system tracks the purity of every expression making it possible to write parts of a Flix program in a purely functional style with purity enforced by the effect system. Flix programs compile to JVM bytecode and are executable on the Java Virtual Machine (JVM).[14] The Flix compiler performs whole program compilation, eliminates polymorphism via monomorphization,[15] and uses tree shaking to remove unreachable code. Monomorphization avoids boxing of primitive values at the cost of longer compilation times and larger executable binaries. Flix has some support for interoperability with programs written in Java.[16] Flix supports tail call elimination which ensures that function calls in tail position never consume stack space and hence cannot cause the call stack to overflow.[17] Since the JVM instruction set lacks explicit support for tail calls, such calls are emulated using a form of reusable stack frames.[18] Support for tail call elimination is important since all iteration in Flix is expressed through recursion. The Flix compiler disallows most forms of unused or redundant code, including: unused local variables, unused functions, unused formal parameters, unused type parameters, and unused type declarations, such unused constructs are reported as compiler errors.[19] Variable shadowing is also disallowed. The stated rationale is that unused or redundant code is often correlated with erroneous code[20] A Visual Studio Code extension for Flix is available.[21] The extension is based on the Language Server Protocol, a common interface between IDEs and compilers being developed by Microsoft. Flix is open source software available under the Apache 2.0 License. ExamplesHello worldThe following program prints "Hello World!" when compiled and executed: def main(): Unit \ IO =
println("Hello World!")
The type and effect signature of the Algebraic data types and pattern matchingThe following program fragment declares an algebraic data type (ADT) named enum Shape {
case Circle(Int32), // has circle radius
case Square(Int32), // has side length
case Rectangle(Int32, Int32) // has height and width
}
The ADT has three constructors: The following program fragment uses pattern matching to destruct a def area(s: Shape): Int32 = match s {
case Circle(r) => 3 * (r * r)
case Square(w) => w * w
case Rectangle(h, w) => h * w
}
Higher-order functionsThe following program fragment defines a higher-order function named def twice(f: Int32 -> Int32): Int32 -> Int32 = x -> f(f(x))
We can use the function twice(x -> x + 1)(0)
Here the call to Parametric polymorphismThe following program fragment illustrates a polymorphic function that maps a function def map(f: a -> b, l: List[a]): List[b] = match l {
case Nil => Nil
case x :: xs => f(x) :: map(f, xs)
}
The Flix supports type parameter elision hence it is not required that the type parameters Extensible recordsThe following program fragment shows how to construct a record with two fields def point2d(): {x: Int32, y: Int32} = {x = 1, y = 2}
Flix uses row polymorphism to type records. The def sum(r: {x: Int32, y: Int32 | rest}): Int = r.x + r.y
The following are all valid calls to the sum({x = 1, y = 2})
sum({y = 2, x = 1})
sum({x = 1, y = 2, z = 3})
Notable featuresPolymorphic effectsThe Flix type and effect system separates pure and impure expressions.[5][22][23] A pure expression is guaranteed to be referentially transparent. A pure function always returns the same value when given the same argument(s) and cannot have any (observable) side-effects. For example, the following expression is of type 1 + 2 : Int32 \ {}
whereas the following expression has the println("Hello World") : Unit \ IO
A higher-order function can specify that a function argument must be pure, impure, or that it is effect polymorphic. For example, the definition of // The syntax a -> Bool is short-hand for a -> Bool \ {}
def exists(f: a -> Bool, xs: Set[a]): Bool = ...
The requirement that A higher-order function can also require that a function is impure. For example, the definition of def foreach(f: a -> Unit \ IO, xs: List[a]): Unit \ IO
The requirement that The type and effect is sound, but not complete. That is, if a function is pure then it cannot cause an effect, whereas if a function is impure then it may, but not necessarily, cause an effect. For example, the following expression is impure even though it cannot produce an effect at run-time: if (1 == 2) println("Hello World!") else ()
A higher-order function can also be effect polymorphic: its effect(s) can depend on its argument(s). For example, the standard library definition of def map(f: a -> b \ e, xs: List[a]): List[b] \ e
The A higher-order function that takes multiple function arguments may combine their effects. For example, the standard library definition of forward function composition def >>(f: a -> b \ e1, g: b -> c \ e2): a -> c \ e1 + e2 = x -> g(f(x))
The type and effect signature can be understood as follows: The The type and effect system allows arbitrary set expressions to control the purity of function arguments. For example, it is possible to express a higher-order function def h(f: a -> b \ e1, g: b -> c \ e2 - e1): Unit
If The type and effect system can be used to ensure that statement expressions are useful, i.e. that if an expression or function is evaluated and its result is discarded then it must have a side-effect. For example, compiling the program fragment below: def main(): Unit \ IO =
List.map(x -> 2 * x, 1 :: 2 :: Nil);
println("Hello World")
causes a compiler error: -- Redundancy Error --------------------------------------------------
>> Useless expression: It has no side-effect(s) and its result is discarded.
2 | List.map(x -> 2 * x, 1 :: 2 :: Nil);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
useless expression.
because it is non-sensical to evaluate the pure expression First-class datalog constraintsFlix supports Datalog programs as first-class values.[6][9][26] A Datalog program is a logic program that consists of a collection of unordered facts and rules. Together, the facts and rules imply a minimal model, a unique solution to any Datalog program. In Flix, Datalog program values can be passed to and returned from functions, stored in data structures, composed with other Datalog program values, and solved. The solution to a Datalog program (the minimal model) is itself a Datalog program. Thus, it is possible to construct pipelines of Datalog programs where the solution, i.e. "output", of one Datalog program becomes the "input" to another Datalog program. The following edge facts define a graph: Edge(1, 2). Edge(2, 3). Edge(3, 4).
The following Datalog rules compute the transitive closure of the edge relation: Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
The minimal model of the facts and rules is: Edge(1, 2). Edge(2, 3). Edge(3, 4).
Path(1, 2). Path(2, 3). Path(3, 4).
Path(1, 3). Path(1, 4). Path(2, 4).
In Flix, Datalog programs are values. The above program can be embedded in Flix as follows: def main(): #{Edge(Int32, Int32), Path(Int32, Int32)} =
let f = #{
Edge(1, 2). Edge(2, 3). Edge(3, 4).
};
let p = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
};
solve f <+> p
The local variable Since Datalog programs are first-class values, we can refactor the above program into several functions. For example: def edges(): #{Edge(Int32, Int32), Path(Int32, Int32)} = #{
Edge(1, 2). Edge(2, 3). Edge(3, 4).
}
def closure(): #{Edge(Int32, Int32), Path(Int32, Int32)} = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
}
def run(): #{Edge(Int32, Int32), Path(Int32, Int32)} = solve edges() <+> closure()
The un-directed closure of the graph can be computed by adding the rule: Path(x, y) :- Path(y, x).
We can modify the def closure(directed: Bool): #{Edge(Int32, Int32), Path(Int32, Int32)} =
let p1 = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
};
let p2 = #{
Path(y, x) :- Path(x, y).
};
if (directed) p1 else (p1 <+> p2)
Type-safe compositionThe Flix type system ensures that Datalog program values are well-typed. For example, the following program fragment does not type check: let p1 = Edge(123, 456).;
let p2 = Edge("a", "b").;
p1 <+> p2;
because in Stratified negationThe Flix compiler ensures that every Datalog program value constructed at run-time is stratified. Stratification is important because it guarantees the existence of a unique minimal model in the presence of negation. Intuitively, a Datalog program is stratified if there is no recursion through negation,[27] i.e. a predicate cannot depend negatively on itself. Given a Datalog program, a cycle detection algorithm can be used to determine if it is stratified. For example, the following Flix program contains an expression that cannot be stratified: def main(): #{Male(String), Husband(String), Bachelor(String)} =
let p1 = Husband(x) :- Male(x), not Bachelor(x).;
let p2 = Bachelor(x) :- Male(x), not Husband(x).;
p1 <+> p2 // illegal, not stratified.
because the last expression constructs a Datalog program value whose precedence graph contains a negative cycle: the The Flix compiler computes the precedence graph for every Datalog program valued expression and determines its stratification at compile-time. If an expression is not stratified, the program is rejected by the compiler. The stratification is sound, but conservative. For example, the following program is unfairly rejected: def main(): #{A(Int32), B(Int32)} =
if (true)
A(x) :- A(x), not B(x).
else
B(x) :- B(x), not A(x).
The type system conservatively assumes that both branches of the if expression can be taken and consequently infers that there may be a negative cycle between the Design philosophyFlix is designed around a collection of stated principles:[28]
The principles also list several programming language features that have been deliberately omitted. In particular, Flix lacks support for:
References
External links
|
Portal di Ensiklopedia Dunia