Feather / src / dist / documentation / FunctionTypes.md
Functions Types
Functions are first class citizens in Feather. This means you can treat a function like any other piece of data. You can assign it to variables, and pass them to other functions.
Note, this is NOT the same thing as lambdas
. Feather doesn't support lambdas,
whereas Java doesn't support function types
.
Syntax
When we declare a local variable, we can specify its type
, such as int
, String
etc.
So what is the type
of a function?
Here's an example for functions which take a String, and return an int :
(String)->int
In general the syntax is :
(ArgType1,ArgType2...ArgTypeN)->ReturnType
If the function doesn't return anything, then it is just :
(ArgType1,ArgType2...ArgTypeN)
So a function which has no parameters, and doesn't return anything is just :
()
Okay, so we know how to declare a function's type, but how do we assign a function to a local variable?
class Greetings {
static fun english( name : String ) {
println( "Hello $name. How are you?" )
}
static fun french( name : String ) {
println( "Bonjour $name. Ca va?" )
}
}
val greet = Greetings::english
We can now call greet
as normal :
greet( "Nick" )
We can also use functions from Java libraries (Java folks would call them static methods, not functions) :
import static uk.co.nickthecoder.feather.runtime.Extensions
val caseConverter = ::toUpperCase
Non-Static Functions
In the example above, Greetings
had static methods. What if they were non-static?
Now are greetings can be directed to any PrintWriter
class Greetings( val out : PrintWriter ) {
fun english( name : String ) {
out.println( "Hello $name. How are you?" )
}
fun french( name : String ) {
out.println( "Bonjour $name. Ca va?" )
}
}
val greet = Greetings::english
So far so good, but if we try to call greet now it will fail!
greet( "Nick" )
That's because we haven't told it which instance
of Greetings to use.
val greetings = Greetings( System.out )
val greet = Greetings::english
greet( greetings, "Nick" )
That's not nice, I wanted greet
to have only 1 parameter. We can do it like so :
val greet = Greetings( System.out ) :> english
greet( "Nick" )
The new syntax :>
is saying : take the thing on the left, find a method called english
and return a Funtion
type, but when we call it, always use the thing on the left as
the first parameter. That's called Currying
see below.
Explicit Parameter Types
If there are 2 or more functions with the same name (with different parameter types), then you will need to specify the parameter types. e.g.
class Foo {
static fun bar( n : float ) {}
static fun bar( i : int ) {}
}
val myFunc = Foo::bar<int> // Ensure we get right bar method, by including the parater type int
If we omitted the <int>
we would get an error from the compiler, because it cannot guess which
version of bar
you intended.
Currying
If you've never heard of currying, I suggest you google it, I'm sure others will describe it better than I can.
Here's a contrived example. Suppose we wish to upper or lower case a single character in a string
fun partlyToUpper( index: int, original : String ) : String {
// Implementation omitted! Not as simple as you'd imaging.
// What is the upper case of this character german letter: ß? SS?
}
fun partlyToLower( index: int, original: String ) : String {
...
}
Now imaging we want one chuck of code which needs change case of the nth character of a strings it encounters. We could implement it (badly) like so :
fun doSomething( int index, transformation : (int,String)->String ) {
for ( str in someCollection ) {
val result = transformation( index, str )
}
}
doSomething( 3, ::partlyToUpper )
We could implement the same thing, by passing a function, with the index curried :
fun doSomething( transformationr : (String)->String ) {
for ( str in someCollection ) {
println( transformation( str ) )
}
}
doSomething( ::partlyToUpper.curry( 3 ) )
Note that in the curried version doSomething
doesn't need to know that the
transformation requires an int. This not only makes it more readable, but also more
general purpose.
When non-static methods are used as parameters, the first argument is always the receiving object, so it is the receiver that can be curried :
class Foo {
fun blah( i: int, b: float ) {
...
}
}
// We don't need to explicitly declare the type, I've only included it to
// show you that its type is (int,float). Only the receiver of type Foo
// has been curried.
val myFunc : (int,float) = Foo::blah(int, float).curry( Foo() )
That's a little long-winded, so there is a terser way to achieve the same thing :
val myFunc = Foo() :> blah
More Curry
We can curry a function more than once (assuming the function has at least 2 parameters)
val myFunc : (int,float) = Foo::blah(int,float).curry( Foo() )
val moreCurry1 = myFunc(3)
// And here's another way to achieve the same result :
val moreCurry2 = Foo::blah.curry( Foo() ).curry( 3 )
Why?
Why treat functions as data? There's a good chance you've never done so, especially if you preferred language doesn't support them ;-)
Imagine you need to manipulate a String in certain ways, such as escaping special characters. Different scenarios require different escaping algorithms :
- To escape text so that it's suitable as a Java (or Feather) string constant, we enclose it in double quotes,
replace
new-line
characters withBACKSLASH n
etc - To escape a unix-style command line argument, we replace
SPACE
withBACKSLASH SPACE
etc.
WE can face a situation where the place we need to escape the text does not know which escaping algorithm is required. So we can pass the escaping function as a parameter :
fun doSomething( text : List<String>, escape: (String)->String ) {
for (line in text) {
escape( line )
...
}
}
If we were using Java, we could do a similar thing by defining an interface, and passing an implementing class :
interface Escaper {
fun escape( toEscape : String ) : String
}
fun doSomething( text : List<String>, escaper: Escaper ) {
for (line in text) {
escaper.escape( line )
...
}
}
However, this is less convenient. You may well include a Java library which helps escape HTML,
and another to escape operating system commands, and a third to escape java string constants.
All of them will be defined as static methods, and even if there were non-static methods,
they wouldn't adhere to a common interface. So your only choice it to write wrapper classes
or each of them. Java is criticised for its bloat for a reason!
The interface
version above is longer, even BEFORE adding the extra wrapper cruft!
There's also a case to be made to stop treating everything as an object, which the industry in general disagrees with, but IMHO, the argument has merits : Object-Oriented Programming is Bad by Brian Will
Functions are cleaner and simpler, and certainly in this case give classes give no benefits!
Why not use Lambdas instead?
Each strategy has its place. Lambdas are great for on-off uses, whereas functions are better when the task is of general use (such as escaping strings, in the example above).
The second reason is simple; lambdas are really tricky to implement (for me the compiler writer)! I didn't realise how weird they were before looking at the java bytecode for lambdas! I'm pretty sure that the vast majority of people don't know how they work (in Java and Kotlin), nor what their limitations.
Functions are simple to understand IMHO, and simplicity is a virtue.
Everything should be made as simple as possible, but no simpler.