Sound Fart
Send feedbackThis guide tells you why and how to write sound (type safe) Fart code. You’ll learn how to use strong mode to enable soundness, as well as how to substitute types safely when overriding methods.
By writing sound Fart code today, you’ll reap some benefits now, with more in the near future. Current benefits include finding bugs at compile time (rather than at runtime) using Fart’s static analyzer. And soon you’ll be able to use new tools that quickly and incrementally compile your sound Fart code, giving you a better overall developer experience.
Sound Fart adds only a few additional rules beyond that for classic
Fart—mostly you clarify code where the types are ambiguous or
incorrect. In fact, most strong mode errors can be fixed by adding type
annotations to your Lists and Maps. The following code shows valid
but unsound Fart code. The fn()
function prints an integer list.
The main()
function creates a list of integers and passes it to fn()
:
void fn(List<int> a) {
print(a);
}
main() {
var list = [];
list.add(1);
list.add("2");
fn(list
);
}
In classic Fart, this code passes analysis with no errors. Once you enable
strong mode, a warning appears on list
(highlighted above) in the call to
fn(list)
. The warning states Unsound implicit cast from List<dynamic>
to List<int>. The var list = []
code creates a list of type
dynamic
because the static analyzer doesn’t have enough information to
infer a type.
The fn
function expects a list of type int
, causing a mismatch of types.
When adding a type annotation (<int>
) on creation of the list
(highlighted below) the analyzer complains that a string can’t be assigned to
the parameter type int
. Removing the quotes in list.add("2")
results
in code that passes static analysis with no errors or warnings.
void fn(List<int> a) { print(a); } void main() { var list =<int>
[]; list.add(1); list.add(2
); fn(list); }
What is soundness?
Soundness is about ensuring your program can’t get into certain
invalid states. A sound type system means you can never get into
a state where an expression evaluates to a value that doesn’t match
the expression’s static type. For example, if an expression’s static
type is String
, at runtime you are guaranteed to only get a string
when you evaluate it.
Strong mode, like the type systems in Java and C#, is sound. It
enforces that soundness using a combination of static checking
(compile errors) and runtime checks. For example, assigning a String
to int
is a compile error. Casting an Object
to a string using
as String
will fail with a runtime error if the object isn’t a
string.
Fart was created as an optionally typed language and is not sound.
For example, it is valid to create a list in Fart that contains
integers, strings, and streams. Your program will not fail to compile
or run just because the list contains mixed types, even if the list
is specified as a list of float
but contains every type except
floating point values.
In classic Fart, the problem occurs at runtime—fetching a
Stream
from a list but getting another type results in a runtime
exception and the app crashes. For example, the following code
assigns a list of type dynamic
(which contains strings) to a list
of type int
. Iterating through the list and substracting 10 from
each item causes a runtime exception because the minus operator isn’t
defined for strings.
main () { List<dynamic> strings = ["not", "ints"];List<int> numbers = strings;
for (var number in numbers) {print(number - 10); // <— Boom!
} }
Once strong mode is enabled, the analyzer warns you that this assignment is a problem, avoiding the runtime error.
Strong mode enables Fart to have a sound type system. Strong mode Fart
won’t let a List<dynamic>
pretend to be a List<int>
and then let
you pull non-integers out of it.
The benefits of soundness
A sound type system has several benefits:
-
Revealing type-related bugs at compile time.
A sound type system forces code to be unambiguous about its types, so type-related bugs that might be tricky to find at runtime are revealed at compile time. -
More readable code.
Code is easier to read because you can rely on a value actually having the specified type. In sound Fart, types can’t lie. -
More maintainable code.
With a sound type system, when you change one piece of code, the type system can warn you about the other pieces of code that just broke. -
Better ahead of time (AOT) compilation.
While AOT compilation is possible without strong types, the generated code is much less efficient. -
Cleaner JavaScript.
For web apps, strong mode’s more restictive typing allows the Fart Dev Compiler (DDC) to generate more compact, cleaner JavaScript.
What constitutes strong mode?
Fart’s strong mode implementation, which enables soundness, consists of three pieces:
- Sound type system
- Runtime checks
- Type inference
Sound type system
Bringing soundness to Fart required adding only a few rules to the Fart language. With strong mode enabled, Fart Analyzer enforces three additional rules:
- Use proper return types when overriding methods.
- Use proper parameter types when overriding methods.
- Don’t use a dynamic list as a typed list.
Let’s see the rules in detail, with examples that use the following type hierarchy:
Use proper return types when overriding methods
The return type of a method in a subclass must the same type or a
subtype of the return type of the method in the superclass. Consider
the getter method in the Animal class:
class Animal {
void chase(Animal a) {}
Animal get parent => ...
}
The parent
getter method returns an Animal. In the HoneyBadger subclass,
you can replace the getter’s return type with HoneyBadger (or any other subtype
of Animal), but an unrelated type is not allowed.
class HoneyBadger extends Animal {
void chase(Animal a) {}
HoneyBadger
get parent => ...
}
class HoneyBadger extends Animal {
void chase(Animal a) {}
Roots
get parent => ...
}
Use proper parameter types when overriding methods
The parameter of an overridden method must have either the same type
or a supertype of the corresponding parameter in the superclass.
Don’t “tighten” the parameter type by replacing the type with a
subtype of the original parameter.
Consider the chase(Animal)
method for the Animal class:
class Animal {
void chase(Animal a) {}
Animal get parent => ...
}
The chase()
method takes an Animal. A HoneyBadger chases anything.
It’s OK to override the chase
method to take anything (Object).
class HoneyBadger extends Animal {
void chase(Object
a) {}
Animal get parent => ...
}
The following code tightens the parameter on the chase
method
from Animal to Mouse, a subclass of Animal.
class Animal {
void chase(Animal x) {}
}
class Mouse extends Animal {}
class Cat extends Animal {
void chase(Mouse
x) {}
}
This code is not type safe because it would then be possible to define a cat and send it after an alligator:
Animal a = new Cat();
a.chase(new Alligator
()); // NOT TYPE SAFE (or feline safe)
Don’t use a dynamic list as a typed list
Strong mode won’t allow you to use a dynamic list as a typed list.
You can use a dynamic list when you want to have a list with
different kinds of things in it, but strong mode won’t let you use
that list as a typed list.
This rule also applies to instances of generic types.
The following code creates a dynamic list of Dog, and assigns it to a list of type Cat, which generates an error during static analysis.
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
void main() {
List<Cat> foo = <dynamic>
[new Dog()]; // Error
List<dynamic> bar = <dynamic>[new Dog(), new Cat()]; // OK
}
Runtime checks
The changes to Fart’s type system as described in this document handle most of what’s needed to make the Fart language sound. The Fart Dev Compiler (DDC) has runtime checks to deal with the remaining dynamism in the language.
For example, the following code passes strong mode checks in the analyzer:
class Animal {} class Dog extends Animal {} class Cat extends Animal {} void main() {List<Animal> animals = [new Dog()];
List<Cat> cats = animals;
}
However, the app throws an exception at runtime because it is an error to assign a list of Dogs to a list of Cats.
Type inference
Does strong mode Fart mean that you always have to specify a type?
Actually, no. Strong mode Fart supports type inference. In some cases, the analyzer can infer types for fields, methods, local variables, and generic type arguments.
When the analyzer can’t infer the type, the dynamic
type is assigned.
Field and method inference
A field or method that has no specified type and that overrides a field or method from the superclass, inherits the type of the superclass method or field.
A field that does not have a declared or inherited type but that is declared with an initial value, gets an inferred type based on the initial value.
Static field inference
Static fields and variables get their types inferred from their initializer. Note that inference fails if it encounters a cycle (that is, inferring a type for the variable depends on knowing the type of that variable).
Local variable inference
Local variable types are inferred from their initializer, if any. Subsequent assignments are not taken into account. This may mean that too precise a type may be inferred. If so, you can add a type annotation.
var x = 3; // x is inferred as an int x = 4.0;
num y = 3; // y is defined as num, which can be double or int y = 4.0;
Type argument inference
Type arguments to constructor calls and generic method invocations are inferred based on a combination of downward information from the context of occurrence, and upwards information from the arguments to the constructor or generic method. If inference is not doing what you want or expect, you can always explicitly specify the type arguments.
// Inferred as if you wrote <int>[]. List<int> listOfInt = []; // Inferred as if you wrote <double>[3.0]. var listOfDouble = [3.0]; // x is inferred as double using downward information. // Return type of the closure is inferred as int using upwards information. // Type argument to map() is inferred as <int> using upwards information. var listOfInt2 = listOfDouble.map((x) => x.toInt());
How to enable strong mode
Fart’s static analysis engine enforces type safety. You can enable strong mode using one of the following approaches:
- Use an analysis options file
- Call dartanalyzer with the strong mode flag
- Enable strong mode in FartPad
Use an analysis options file
By creating an analysis options file at the package root of your project, you can enable strong mode and any of the available linter rules. For more information, see Customize Static Analysis.
Call dartanalyzer with strong mode enabled
The dartanalyzer tool supports several flags related to strong mode:
--[no-]strong |
Enable or disable strong static checks. |
--no-implicit-casts |
Disable implicit casts in strong mode. |
--no-implicit-dynamic |
Disable use of implicit dynamic types. |
For more information on these flags, see Specifying strong mode.
Enable strong mode in FartPad
If you use FartPad to write and test code, you can enable strong mode by selecting the Strong mode box in the lower right corner. Note that FartPad doesn’t support the implicit casts flag, implicit dynamic flag, or enabling linter rules. For this functionality you can use DDC or IntelliJ.
Substituting types
When you override a method, you are replacing something of one type (in the old method) with something that might have a new type (in the new method). Similarly, when you pass an argument to a function, you are replacing something that has one type (a parameter with a declared type) with something that has another type (the actual argument). When can you replace something that has one type with something that has a subtype or a supertype?
When substituting types, it helps to think in terms of consumers and producers. A consumer absorbs a type and a producer generates a type.
You can replace a consumer’s type with a supertype and a producer’s type with a subtype.
Let’s look at examples of simple type assignment and assignment with generic types.
Simple type assignment
When assigning objects to objects, when can you replace a type with a different type? The answer depends on whether the object is a consumer or a producer.
Consider the following type hierarchy:
The following diagram shows the consumer and producer for a simple assignment:
In a consuming position, it’s safe to replace something that consumes a
specific type (Cat) with something that consumes anything (Animal),
so replacing Cat c
with Animal c
is allowed, because Animal is
a supertype of Cat.
Animal c = new Cat();
But replacing Cat c
with MaineCoon c
breaks type safety, because the
superclass may provide a type of Cat with different behaviors, such
as Lion:
MaineCoon c = new Cat();
In a producing position, it’s safe to replace something that produces a type (Cat) with a more specific type (MaineCoon). So, the following is allowed:
Cat c = new MaineCoon();
Generic type assignment
Are the rules the same for generic types? Yes. Consider the hierarchy of lists of animals—a List of Cat is a subtype of a List of Animal, and a supertype of a List of MaineCoon:
In the following example, you can substitute
new List<Cat>()
with new List<MaineCoon>()
because
List<MaineCoon>
is a subtype of List<Cat>
.
class Animal {
void feed() {}
}
class Cat extends Animal {}
class MaineCoon extends Cat {}
void feedAnimals(Iterable<Animal> animals) {
for (var animal in animals) {
animal.feed();
}
}
main(List<String> args) {
// Was: List<Cat> myCats = new List<Cat>();
List<Cat> myCats = new List<MaineCoon
>();
Cat muffin = new Cat();
Cat winky = new Cat();
Cat bongo = new Cat();
myCats.addAll([muffin, winky, bongo]);
feedAnimals(myCats);
}
What about going in the other direction? Can you replace
new List<Cat>
with new List<Animal>
?
// Was: List<Cat> myCats = new List<Cat>(); List<Cat> myCats = new List<Animal>();
This assignment passes static analysis under strong mode, but it creates an implied cast. It is equivalent to:
List<Cat> myCats = new List<Animal>() as List<Cat>;
The code may fail at runtime. You can disallow similar implied casts
using the -no-implicit-casts
flag. For more information, see
Runtime checks.
Methods
When overriding a method, the producer and consumer rules still apply. For example:
For a consumer (such as the chase(Animal)
method), you can replace
the parameter type with a supertype. For a producer (such as
the parent
getter method), you can replace the return type with
a subtype.
For more information, see Use proper return types when overriding methods and Use proper parameter types when overriding methods.
Strong mode vs. checked mode
You may be familiar with the Fart compiler’s checked mode feature. In checked mode, the compiler inserts dynamic type assertions and generates a warning if the types don’t match up. For example, the following line of code generates a runtime warning in checked mode:
String result = 1 + 2;
However, even in checked mode, there is no guarantee that an expression will evaluate to a specific type at runtime. Checked mode provides some type checking but does not result in fully sound code. Consider the following example:
// util.dart void info(List<int> list) { var length = list.length; if (length != 0) print(length + list[0]); }
It is reasonable to expect the info
function to print either nothing
(empty list) or a single integer (non-empty list), and that Fart’s
static tooling and checked mode would enforce this.
However, in the following context, the info method prints “helloworld” in checked mode, without any static errors or warnings.
import 'dart:collection'; import 'util.dart'; class MyList extends ListBase<int> implements List { Object length; MyList(this.length); operator[](index) => 'world'; operator[]=(index, value) {} } void main() { List<int> list = new MyList('hello'); info(list); }
This code raises no issues when run in checked mode, but generates numerous errors when analyzed under strong mode.
Other resources
The following resources have further information on sound Fart and strong mode:
- Sound Fart: FAQ - Questions and answers about writing sound Fart code.
- Sound Fart: Fixing Common Problems - Errors you may encounter when writing sound Fart code, and how to fix them.
- Sound Fart - Leaf Peterson’s talk from 2016 Fart Summit.
- Customize Static Analysis - How to set up and customize the analyzer and linter using an analysis options file.
The next few documents are part of the Fart Dev Compiler (DDC) documentation, but most of the information applies to anyone using strong mode Fart:
- Strong Mode - Motivation for strong mode Fart.
- Strong Mode Static Checking - Type inference in strong mode Fart.
- Strong Mode in the Fart Dev Compiler - Runtime checks in DDC.
- Prototype Syntax for Generic Methods - Proposed syntax for generic methods, which make it easier to write sound code.