Beauty is in the eye of the beholder - but we are obsessed with making the Fantom APIs beautiful. The Java and .NET APIs have developed over the years into a somewhat tangled mess. Some APIs are just plain bad - Java's
Calendar class is the poster child for APIs which are just miserable to use. You have to use all of their weird C like constants for access, months are freaking zero based, but weekdays are one based!
Some of this is normal cruft setting in, but much of it is a general philosophy in both Java and .NET API design. Both platforms tend toward APIs using a proliferation of small classes that are over abstracted and under powered. Fantom follows a very different philosophy - we believe in a very few, but powerful classes. A good example is the
java.io package which contains over 60 classes and interfaces. To do anything useful requires three of four classes, and if you forget to use buffered streams, then performance goes to hell. And even with all of these classes, it is still a lot of work to do basic things like parse a file into lines of text. Fantom collapses most of the
java.io functionality into four classes:
OutStream. The IO streams classes are buffered by default, support both binary and text, and have lots of conveniences built right in.
Strong versus Dynamic Typing
The industry has developed a schism between proponents of strong typing and those of dynamic typing. Frankly we find both sides too extreme for our taste, so Fantom takes a middle of the road, moderate approach to its type system.
On the strong typing side, Fantom requires you to annotate field and method signatures with types. We think this is a good thing. Programming is about constructing well defined contracts between software components - type systems aren't perfect, but they do a pretty good job for documenting and defining these contracts. If I want to write a method which expects a Str and returns an Int, then that should be captured right in the code.
Beyond annotating field and method signatures with types, Fantom takes a laissez faire attitude towards type declaration. Type inference is often used for local variables and collection literals.
Sometimes you just really need dynamic typing. One of Fantom's pivotal features is the ability to call a method using strong or dynamic typing. If you call a method via the "." operator, the call is type checked by the compiler and compiled into an efficient opcode. But you can also use the "->" operator to call a method dynamically. This operator skips type checking, and can be used to implement duck typing. The "->" operator actually routes to the
Obj.trap method which can be overridden to build all sorts of nifty dynamic designs.
Interestingly enough while Fantom is trying to make programs less strongly typed, the Java and C# languages are moving to be more strongly typed. Generic types illustrate this trend - a feature added to both Java and C# in the not so distant past. A fully parameterized type system introduces a great deal of complexity - we are trying hard to find the right balance between value and complexity.
Currently Fantom takes a limited approach to generics. There is no support for user defined generics yet. However, three built-in classes
Func can be parameterized using a special syntax. For example a list of
Ints in Fantom is declared as
Int using the familiar array type syntax of Java and C#. This trade-off seems to hit the sweet spot where generics make sense without complicating the overall type system.
Designing software to be modular is one of those things you learn in CS 101 - it is fundamental to good design. Modular software should let you easily divide your programs up into reusable chunks which are easy to version, ship around, and combine with other modules via clear dependencies.
What passed for module management in Java for decades is the JAR file - which is basically to say Java really didn't have any module management. Project Jigsaw was was finally released in Java 9, but it has an uphill battle to conquer the inertia from so many years without a proper module system. And its unnecessarily complex to compensate for overlaying modules over a system orginally designed without modules in mind.
The .NET design was pretty serious about modularity, and at the high level it has a great design for versioning, GAC, etc. However when it comes to the details, .NET leaves a lot to be desired. Where Java chose ZIP as a simple, flexible way to package up files, .NET uses opaque DLLs with all sorts of Window's specific cruft that makes .NET module files difficult to work with. And to require a separate, undocumented debug pdb file to get meaningful stack traces is just plain wrong.
Everything in Fantom is designed around modular units called pods. Pods are the unit of versioning and deployment. They are combined together using clear dependencies. Like Java they are just ZIP files which can be easily examined.
Namespace versus Deployment
Java and .NET to a lesser degree separate the concepts of namespace and deployment. For example in Java packages are used to organize code into a namespace, but JAR files are used to organize code for deployment. The problem is there isn't any correspondence between these concepts. This only exacerbates classpath hell - you have a missing class, but the class name doesn't give you a clue as to what JAR file the class might live in.
This whole notion of type namespaces versus deployment namespaces does offer flexibility, but also seems like unnecessary complexity. Fantom takes a simple approach to managing the namespace using a fixed three level hierarchy "pod::type.slot". The first level of the namespace is always the pod name which also happens to be the unit of deployment and versioning. This consistency becomes important when building large systems through the assembly of pods and their types. For example, given a serialized type "acme::Foo", it is easy to figure out what pod you need.
One of the most important trade-offs made in the design of Java was primitive types. Since primitives aren't really Objects, they become an anomaly which results in all sorts of ugly special cases. On the other hand, primitives are important in achieving C like performance - especially for numeric applications. Java has since put a band-aid on primitives with auto-boxing - but the type system remains fractured.
.NET tackles the problem quite elegantly with value types. These are special types which have the performance of primitives, but they still cleanly subclass from
Fantom follows the .NET model of value types. The three special types
Float are value types which are implemented as primitives in Java and value types in .NET. These types have all the same performance characteristics of using
double in Java or C#. Unlike Java these types cleanly subclass from
Obj to create a unified class hierarchy. The compiler automatically implements boxing and unboxing when necessary.
When Fantom was originally developed, Java did not provide much in the way for functional programming. Java 8 did add many features to support functional programming, but functions are still are not truly first class citizens in the type system.
Fantom was designed from the ground up to support functions as first class objects. Closures are a key feature of the language, and all the APIs are written to use functions and closures where appropriate.
Quite often we need to declare data structures in our code. Common examples include declaring a list or map. In Java and C# these simple tasks include mostly noise which makes for very ugly, verbose declarative programming. For this reason, you often find the declarative parts of a Java or C# application shoved off into XML files.
Fantom incorporates declarative programming right into the language. Fantom supports a literal syntax for lists, maps, ranges, uris, and durations. Fantom also includes a text serialization syntax which is human readable and writable. The serialization syntax is a clean subset of the programming language - so you can paste a serialization file right into your source code as an expression.
Most main stream languages today use a shared state model - all threads share the same memory space, and programmers must be diligent about locking memory in order to prevent race conditions. If locks are used incorrectly, then deadlocks can occur. This is a fairly low level approach to managing concurrency. It also makes it quite difficult to create composable software components.
Fantom tackles concurrency using a couple techniques:
- Immutability is built into the language (thread safe classes)
- Static fields must be immutable (no shared mutable state)
- Actors model for message passing (Erlang style concurrency)
The beauty of a new language is that it gives you a clean slate to fix all the little things that aggravate you (we built Fantom to scratch our own itches). Other little things we included in Fantom which we found frustrating about Java:
- Default parameters: methods can have default arguments - no more writing boiler plate code for convenience methods
- Type Inference: local variables use type inference
- Field Accessors: field accessors are defined implicitly - another area where idiomatic Java code has a low signal to noise ratio
- Nullable Types: declare your intention in code whether null is a legal value for a parameter or return type instead of relying on documentation
- Checked Exceptions: checked exceptions are evil syntax salt. Checked exceptions don't scale, don't version, and don't allow composable systems - all the reasons why Anders Hejlsberg didn't include checked exceptions in C#.
- Numeric Precision: Fantom doesn't include support for 32-bit, 16-bit, and 8-bit integers and floats. There is only a 64-bit Int and a 64-bit Float. This eliminates a lot of complexity associated with precision problems such as file lengths, Unicode characters, or very large lists. Although much of the Java and C# implementation is 16-bit chars and 32-bit ints under the covers, the Fantom APIs themselves are future proof.