Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor interoperability section #161

Merged
merged 22 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
- [Classes and Interfaces](./extending-classes-and-interfaces.md)
- [Nested and Inner Classes](./nested-and-inner-classes.md)
- [Exceptions](./exceptions.md)
- [Boxing and Unboxing](./boxing-and-unboxing.md)
- [Java Collections](./java-collections.md)
- [Everyday Programming](./everyday-programming.md)
- [The Main Function](./main.md)
Expand Down
37 changes: 37 additions & 0 deletions src/boxing-and-unboxing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Boxing and Unboxing

> **Note:** Requires Flix 0.49.0

Unlike Java, Flix never performs implicit boxing or unboxing of values.

We believe auto boxing is a design flaw and do not plan to support it. Hence,
primitive values must be manually boxed and unboxed.

### Boxing

The following example shows how to box a primitive integer:

```flix
def f(x: Int32): String \ IO =
let i = Box.box(x); // Integer
i.toString()
```

Here the call to `Box.box(x)` returns an `Integer` object. Since `i` is an
object, we can call `toString` on it. Boxing is a pure operation, but calling
`toString` has the `IO` effect.

### Unboxing

The following example shows how to unbox two Java `Integer` objects:

```flix
import java.lang.Integer

def sum(x: Integer, y: Integer): Int32 =
Box.unbox(x) + Box.unbox(y)
```

Here the call to `Box.unbox` returns an `Int32` primitive value.

Unboxing is a pure operation.
217 changes: 181 additions & 36 deletions src/calling-methods.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,209 @@
## Invoking Object Methods
## Calling Object Methods

We can use the import mechanism to invoke methods on objects.
> **Note:** Requires Flix 0.49.0

In Flix, we can call methods on Java objects using syntax similar to Java.

For example:

```flix
import java.io.File

def main(): Unit \ IO =
let f = new File("foo.txt");
println(f.getName())
```

Here we import the `java.io.File` class, instantiate a `File` object, and then
call the `getName` method on that object.

Like with constructors, Flix resolves the method based on the number of
arguments and their types.

Here is another example:

```flix
import java.io.File

def main(): Unit \ IO =
let f = new File("foo.txt");
if (f.exists())
println("The file ${f.getName()} exists!")
else
println("The file ${f.getName()} does not exist!")
```

And here is a larger example:

```flix
import java.io.File
import java.io.FileWriter

def main(): Unit \ IO =
let f = new File("foo.txt");
let w = new FileWriter(f);
w.append("Hello World\n");
w.close()
```

In the above example, we may want to catch the `IOException` that can be raised:

```flix
import java.io.File
import java.io.FileWriter
import java.io.IOException

def main(): Unit \ IO =
let f = new File("foo.txt");
try {
let w = new FileWriter(f);
w.append("Hello World\n");
w.close()
} catch {
case ex: IOException =>
println("Unable to write to file: ${f.getName()}");
println("The error message was: ${ex.getMessage()}")
}
```

## Calling Static Methods

In Flix, we can call static methods (i.e. class methods) using syntax similar to Java:

For example:

```flix
import java_new java.io.File(String): ##java.io.File \ IO as newFile;
import java.io.File.exists(): Bool \ IO as fileExists;
let f = newFile("HelloWorld.txt");
fileExists(f)
import java.lang.Math

def main(): Unit \ IO =
let n = Math.sin(3.14);
println(n)

```

Like with constructors and methods, Flix resolves the static method based on the
number of arguments and their types.

Here is another example:

```flix
import java.lang.Math

def main(): Unit \ IO =
println(Math.abs(-123i32));
println(Math.abs(-123i64));
println(Math.abs(-123.456f32));
println(Math.abs(-123.456f64))
```

## Calling Constructors and Methods with VarArgs

TBD

## When Constructor or Method Resolution Fails

In some cases the Flix compiler is unable to determine what Java constructor or
method is called.

For example, in the program:

```flix
import java.lang.{String => JString}

def f(): String \ IO =
let o = ???;
JString.valueOf(o)
```

Here we import the `java.io.File.exists` method under the name `fileExists`.
The type of `o` is unknown, hence Flix cannot know if we want to call
`String.valueOf(boolean)`, `String.valueOf(char)`, `String.valueOf(double)`, or
one of the other overloaded versions.

If the Java method name is a legal Flix name and we want to reuse it,
we can also import the method without an `as` clause. For example:
The solution is to put a type ascription on the relevant argument:

```flix
import java_new java.io.File(String): ##java.io.File \ IO as newFile;
import java.io.File.exists(): Bool \ IO;
let f = newFile("HelloWorld.txt");
exists(f)
import java.lang.{String => JString}

def f(): String \ IO =
let o = ???;
JString.valueOf((o: Bool))
```

Here we import the method under the name `exists`.
The type ascription specifies that `o` has type `Bool` which allows method
resolution to complete successfully. Note that the extra pair of parenthesis is
required.

## Calling Object Methods through Static Fields

When a Java method is imported, we must annotate it with its effect.
Most commonly, a Java method has a side-effect (such as deleting a file),
and hence must be annotated with the `IO` effect.
We may want to write:

In rare cases where a method is pure, we can import it as such by
writing the empty effect set: `{}`. For example:
```flix
import java.lang.System

def main(): Unit \ IO =
System.out.println("Hello World!")
```

But due to a limitation of the Flix parser, the above has to be written as:

```flix
import java.lang.String.startsWith(String): Bool \ {};
startsWith("Hello World", "Hello")
import java.lang.System

def main(): Unit \ IO =
(System.out).println("Hello World!")
```

And as another example:
## Calling Java Methods Known to be Pure

Any Flix expression that creates a Java object, calls a Java method, or calls a
Java static method has the `IO` effect. This is to be expected: Java
constructors and methods may have arbitrary side-effects.

If we know for certain that a Java constructor or method invocation has no
side-effects, we can use an `unsafe` block to tell Flix to treat that expression
as pure.

For example:

```flix
import java.lang.String.charAt(Int32): Char \ {};
charAt("Hello World", 2)
import java.lang.Math

def pythagoras(x: Float64, y: Float64): Float64 = // Pure, no IO effect
unsafe Math.sqrt((Math.pow(x, 2.0) + Math.pow(y, 2.0)))

def main(): Unit \ IO =
println(pythagoras(3.0, 4.0))
```

Type signatures should use Flix type names and not
Java type names for primitive types.
For example, if a Java method takes a `Double` its
signature should use the Flix type `Float64`.
Similarly, if a Java method takes a `Boolean` its
signature should use the Flix type `Bool`.
This goes for return types, too.
Here we know for certain that `Math.pow` and `Math.sqrt` are _pure_ functions,
hence we can put them inside an `unsafe` block. Thus we are able to type check
the Flix `pythagoras` function as pure, i.e. without the `IO` effect.

## Invoking Static Methods
> **Warning:** Do not, under any circumstances, use `unsafe` on expressions that
> have side-effects. Doing so breaks the type and effect system which can lead
> to incorrect compiler optimizations which can change the meaning of your
> program in subtle or catastrophic ways!

magnus-madsen marked this conversation as resolved.
Show resolved Hide resolved
We can invoke a _static_ method by writing the
`static` keyword after import:
## Partial Application of Java Constructors and Methods

Flix supports partial application of Flix functions. However, Java constructors
and methods can never be partially applied. This limitation can be overcome
introducing an explicit lambda.

For example:

```flix
import static java.lang.String.valueOf(Bool): String \ {};
valueOf(true)
import java.lang.{String => JString}

def main(): Unit \ IO =
def replaceAll(s, src, dst) = s.replaceAll(src, dst);
let f = replaceAll("Hello World");
let s1 = f("World")("Galaxy");
let s2 = f("World")("Universe");
println(s1);
println(s2)
```

Here we introduce a Flix function `replaceAll` which calls `String.replaceAll`.
Since `replaceAll` is a Flix function, we can partially apply it as shown in the
example.
75 changes: 49 additions & 26 deletions src/creating-objects.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,64 @@
## Creating Objects

We can import the constructor of a Java class as a
Flix function and use it to construct new objects.
> **Note:** Requires Flix 0.49.0

In Flix, we can create objects using syntax similar to Java.

For example:

```flix
import java_new java.io.File(String): ##java.io.File \ IO as newFile;
newFile("HelloWorld.txt")
import java.io.File

def main(): Unit \ IO =
let f = new File("foo.txt");
println("Hello World!")
```

Here we import the `java.io.File` class and instantiate a `File` object by
calling one of its constructors using the `new` keyword.

The `File` class has multiple constructors, so we can also write:

```flix
import java.io.File

def main(): Unit \ IO =
let f1 = new File("foo.txt");
let f2 = new File("bar", "foo.txt");
println("Hello World!")
```

Here we import the constructor of the `java.io.File`
class and give it the local name `newFile`.
The `newFile` function takes a string argument and
returns a fresh Java `File` object.
Constructing a fresh object is impure, hence `main`
is marked as having the `IO` effect.
Flix resolves the constructor based on the number of arguments and their types.

As another example, we can write:

When we import a constructor, we must specify the
types of its formal parameters. This is required because
Java supports constructor overloading (i.e. a class may
have multiple constructors only distinguished by their
formal parameters.)
```flix
import java.io.File
import java.net.URI

For example, the `java.io.File` class has another
constructor that takes two arguments: one for the parent
pathname and one for the child pathname.
We can use this constructor as follows:
def main(): Unit \ IO =
let f1 = new File("foo.txt");
let f2 = new File("bar", "foo.txt");
let f3 = new File(new URI("file://foo.txt"));
println("Hello World!")
```

We can use a _renaming import_ to resolve a clash between a Java name and a Flix
module:

```flix
import java_new java.io.File(String, String): ##java.io.File \ IO as newFile;
newFile("foo", "HelloWorld.txt")
import java.lang.{String => JString}

def main(): Unit \ IO =
let s = new JString("Hello World");
println("Hello World!")
```

Here the import describes that the constructor expects two
`String` arguments.
Here `JString` refers to the Java class `java.lang.String` whereas `String`
refers to the Flix module. Note that internally Flix and Java strings are the
same.

> **Note:** Any interaction with Java code always has the `IO` effect.

> **Note:** `import` statements must occur at the expression-level,
> i.e. they must occur inside a function. Unlike `use` declarations,
> they cannot occur at top of a module.
> **Note:** In Flix, Java classes must be `import`ed before they can be used. In
> particular, we _cannot_ write `new java.io.File(...)`.
Loading
Loading