Microsoft took something of a risk with the Visual Studio 2010 release, adding the first brand-new “out of the box” language to the .NET ecosystem since its introduction of C# back in 2001. I speak, of course, of VisualF#, an object/functional hybrid language based on work done by Microsoft Research. Providing both leading-edge functional programming and familiar object-oriented capabilities, F# gives the developer more power to solve problems -- all in fewer lines of code, thanks to succinct syntax.
For developers who have never looked at F# before (a vast majority of the .NET developer population), this article takes a quick glance at the F# syntax. I want to give the F# neophyte enough familiarity with the language to understand code samples in other, more in-depth, articles. Full disclosure dictates, however, that F# is a rich and powerful language, and cannot be explained in all nuance in a single article. The concepts and reasoning of functional programming can only be hinted at in this space; more articles and demonstrations of the power of the “functional” side of F#’s “object/functional hybrid” nature will appear on this website in the weeks and months to come. Welcome to the tip of the iceberg.
Basics
Consider the following F# program, which I created using the Visual Studio “F# Application” project template that ships with Visual Studio 2010:
let msg = "Hello, F#!" System.Console.WriteLine(msg)
Now ponder the following truths about F#:
.NET Accessibility. Observe that F# is capable of calling into the .NET Base Class Library (BCL), as evidenced by the above use of the Console class. F# comes with its own libraries, of course (including the “printfn” function, which serves much the same purpose as WriteLine), but has full access to the entirety of the BCL, including WPF, WCF, Workflow, System.Xml, System.Web, and any other assembly accessible to C# or VB.NET. Referencing them uses the same “Add Reference” mechanism in Visual Studio as any other .NET project.
.NET Assembly. Careful inspection of the compiled artifact, using a tool such as Reflector or ILDasm, reveals that the F# compiler produces an executable assembly similar to those produced by C# or VB.NET. Although the structure of the IL generated from the F# compiler looks somewhat different from that produced by the C# or VB.NET compilers, it is still IL, and therefore any library produced by F# is fully accessible from C# or VB.NET. There is even an F# Library project template in Visual Studio. In formal .NET terminology, this and the previous paragraph mean that F# is a “CLI Consumer” (able to consume other CLI assemblies) and a “CLI Producer” (able to produce assemblies for use by other CLI languages).
Strongly-typed, type inferenced. F# is strongly-typed, like C#, meaning that attempts to use the “msg” value defined above as an integer will yield compiler errors—the compiler knows that “msg” must be a string (a System.String, to be precise) because it infers that from the right-hand side of the assignment. However, unlike C# which can only do this for “var”-declared local variables, F# can do this type inference throughout the entire program (with some exceptions), which has the nice effect of reducing the amount of code that must be typed and read without losing the benefits of strong type-checking at compile-time.
Top-level declarations. Contrary to the C# equivalent of this program, F# does not require the programmer to define a class and a static Main() method to serve as the entry point, although it is perfectly happy to allow the developer to do exactly that. F# by default assumes that the first line in the file is to be executed, and unless told otherwise will do precisely that. More importantly, F# permits top-level declarations of values without requiring a class surrounding it, which can simplify certain code scenarios immensely.
(To be strictly correct, F# then takes the code written above and enfolds it inside a class-like declaration that matches the name of the file, so technically this code is inside a class named “Program”, matching the default “Program.fs” filename. This is of interest to those looking to call F# from C#.)
More Basics
A “Hello world” program, regardless of how familiar, offers only limited analysis opportunities. A slightly more complex program, which provides dietary advice while illustrating arrays and looping:
let quantityOfCookies = [| 1; 2; 3; 4; 5; 0; |] let zero = quantityOfCookies.[5] quantityOfCookies.[5] <- 6for i in quantityOfCookies do if i = zero then printfn "Good job. Eat healthy food." else if i = 1 then printfn "One cookie won’t hurt you." else if i = 2 then printfn "Two cookies is a nice snack." else printfn "%d cookies is too many!" i
This is clearly an equally trivial program, but demonstrates several things at once:
Arrays. Arrays are available in F# (though generally discouraged in favor of lists, which will be seen next), and behave much as they do in C# or VB.NET, although the declaration is different, as seen in “quantityOfCookies”. Accessing the array is also slightly different from C# or VB.NET, in that the “.” must precede the square brackets. In keeping with the CLR platform as a whole, arrays are 0-indexed, as they are in C#.
Decision-making. A single equals (“=”) is used to test for equality in if/then branching, not the double-equals (“==”) familiar to C# programmer. Assignment in F# is done using the leftward-arrow operator, as seen above where the sixth element of quantityOfCookies is set to the value 6. (This ability to in-place modify the contents of the array is part of the reason arrays are discouraged in F#, and in functional languages as a whole. It is considered bad form to change the value in a functional language, leading many to refer to this kind of assignment as a destructive update.)
Looping. F# supports while and do/while loops, just as it does the “for” loop demonstrated above. As can be seen, one form of the “for” loop is to iterate across the contents of an array or list or other collection, just as “foreach … in” does in C#. Other forms are available, including straight integer range steps, but this is one of the more common idioms using it.
Indentation-sensitivity. The F# language does not use any sort of paired parentheses or “begin”/”end” markers to denote blocks of code; instead, it is indentation-sensitive, in that the indentation of code offers declaration of what code is in which nested block, much like the scripting language Python. Visual Studio will assist with ensuring the indentation is correct, but in general F#ers feel that the indentation becomes intuitive and falls away from notice before long.
We can tighten up our dietary advice program using a powerful feature of F# known as pattern-matching. It is well beyond the scope of this article to describe pattern-matching in its complete depth, but the following example gives some idea of its use:
for i in [1 .. 6] do let printMsg = match i with | 0 -> "Good job. Eat healthy food." | 1 -> "One cookie won’t hurt you." | 2 -> "Two cookies are a nice snack." | n when n > 2 -> n.ToString() + " cookies are way too many!" | _ -> failwith("Number out of expected range!") printfn "%s" printMsg
The pattern-match construct, on the surface, looks vaguely similar to a switch/case from C#, in that we see a test condition on the left, with a body of code on the right, separated by an arrow. Multiple match clauses are separated by a vertical pipe (“|”). However, pattern-matching can also bind the value being matched (“i”, each time through the loop) into a local value for use in the body of the match clause, such as the use of “n.ToString()” in the second-to-last clause. In that same clause we also see a guard condition: because we don’t want to match negative values, an added “when” clause restricts the match to “n” only when “n” is greater than 3. When present, a guard condition must be met in order for the match to occur. Finally, matching against “_” is a wildcard match, much as “default” works in C#. Note also that a pattern-match is an expression, and thus yields a value (the string constants declared in the body of the match clauses), allowing it to be assigned to the local “printMsg” value for later use. In the last/default case, the “failwith” expression generates a standard .NET exception.
Pattern-matching is used far and wide within F# code, and any student of the language would be well-advised to be comfortable with its myriad capabilities.
Objects
From its inception, F# embraced the “object-oriented” in its nature as much as its functional abilities. This means it can define classes that are every bit as “O-O”-ish as their C# counterparts. Defining a class type in F# can, in many cases, be even shorter than C# equivalents, owing to the type-inference and indentation-sensitivity. A class representing an individual human and the derived subtype describing a student of some educational facility looks like the following:
type Person(first : string, last : string, age : int) = member p.FirstName = first member p.LastName = last member p.Age = age member p.SayHowdy() = System.Console.WriteLine("{0} says, 'Howdy, all'!", p.FirstName) override p.ToString() = System.String.Format("[Person: first={0}, last={1}, age={2}]", p.FirstName, p.LastName, age) let jess = new Person("Jessica", "Kerr", 33) jess.SayHowdy() type Student(first : string, last : string, age : int, subj : string) = inherit Person(first, last, age) let mutable subject = subj member s.Subject with get() = subject and set(value : string) = subject <- value override s.ToString() = System.String.Format("[Student: subject={0} {1}]", s.Subject, base.ToString()) member s.DrinkBeer(?amt : int, ?kind : string) = let beers = match amt with Some(beers) -> beers | None -> 12 let brand = match kind with Some(name) -> name | None -> "Keystone" System.Console.WriteLine("{0} drank {1} {2}s!", s.FirstName, beers, brand) let brian = new Student("Brian", "Randell", 42, "VB.NET") brian.DrinkBeer(6) brian.DrinkBeer(12, "Coors") brian.DrinkBeer()
The structure of the code is similar in many ways to C# or VB.NET: a type name encloses a series of declarations of members on the type. F# does this without explicit begin/end brackets, using its indentation-sensitive syntax to denote scope. Other differences:
Constructors. Unlike C# or VB.NET which require a constructor to be defined as an explicit member within the class, F# uses the type-declaration line to declare the primary constructor, the one intended to represent the principal means of construction for this class. Any other constructors can be declared inside the class, but will defer to the primary for the actual work, as is generally the case for C# and VB.NET classes, as well.
Immutability. As is common with languages that derive, even in part, from functional roots, F# believes strongly in the notion of “immutability”, that once a value has been set, it cannot be changed. This is true both in the local variables “jess” and “brian”, as well as in the default declarations within the class Student. In fact, to help signify this notion of immutability, “jess” and “brian” will often be referred to as local “values”, not “variables”, since their values cannot vary. (These are also sometimes called “bindings” or “value bindings”, as in “binding a name to a value”.) When working with mutable values, assigning a new value requires use of the “left-arrow” operator, written as “<-“. This is sometimes called the “destructive update” operator (mentioned earlier), both since it “destroys” the value stored there previously and because the negative connotations remind programmers that immutability is better than mutability.
Members. F# uses the “member” keyword to denote the declaration of a public member—method, field, property, event, and so on —as opposed to the “let” syntax to declare an internally-accessible field. All the properties declared in Person are read-only, in keeping with F#’s “immutable values by default” mantra. A read-write property is demonstrated in Student: declare the property using the extended “with get() = … and set() = …” syntax to define get and set method blocks. In addition, since writable properties generally require mutable places to which to write the new values, declaration of the mutable “subject” field is also required. Again we see the destructive update syntax “<-“ to do the assignment. Only fields declared with “let” can be mutable, and these are always private to the type. Members are public unless declared private.
Self-identifiers. Glance at the member declarations: each has a “prefix” in front of the member name (such as “p.FirstName” in Person). This is the “self-identifier” -- the F# developer to choose the name of the reference to object upon which the method is being invoked. For the C#-trained, it may be helpful to write the Person definition of ToString() slightly more C#-ish:
override this.ToString() = System.String.Format("[Person: first={0}, last={1}, age={2}]", this.FirstName, this.LastName, age)
Note that the self-identifier has access to all the members of Person; access to private fields or constructor parameters such as “age” is direct. The choice of name for the self-identifier is entirely up to the F# developer; consistency here, however, will be vital to long-term maintenance.
Inheritance. The Student type inherits from the Person class in the same way as any other C# or VB.NET class, by declaring the primary constructor on the “type” line and then using the “inherit” keyword to specify the type from which it inherits (Person). Note that in keeping with traditional inheritance rules in .NET, some constructor on the base type must be invoked, which is done by passing the parameters to the base type declaration. Note as well that to override base-class members, the “override” keyword is required.
Optional parameters. One of the things F# introduces is the ability to supply defaults to method parameters, using the “?” prefix to the parameter name. This means the parameter type is actually an “option” type, which is either a Some() of some value, or None, both of which can be pattern-matched using the syntax above. We’ll look at this in a future article.
Functions
The parts of F# unfamiliar to most .NET developers center on its functional language nature, meaning that functions—executable code—are first-class values alongside data elements. This means that they can be defined inline, defined in terms of other functions (known in some circles as “partial application” of functions), and passed to other functions for execution inside a particular context (the latter of which is known in academic circles as “higher-order functions”). Developers familiar with LINQ will already be familiar with this concept to some degree—“lambdas” are frequently passed in to LINQ expressions, making LINQ itself a series of higher-order functions.
For an example of this style of programming, consider a collection of Person objects defined above:
let people = [ new Person("Ted", "Neward", 40); new Person("Charlotte", "Neward", 39); new Person("Jessica", "Kerr", 33); new Person("Ken", "Sipe", 42); new Person("Mark", "Richards", 45); new Person("Michael", "Neward", 17); ]
In itself, this list of Persons (formally declared in F# as a “Person list”, as hovering over the declaration of “people” in Visual Studio will reveal) is interesting, but often programmers will want to deduce things about this particular group of people. For example, the goal may be to determine what the total age of all these people, as perhaps as the first step towards determining the average age. While this can easily be done using a “for” loop, as in:
let mutable totalAge1 = 0for p in people do totalAge1 <- totalAge1 + p.Age;
… it turns out that this is a common operation to do. Writing this “for” loop over and over again is a violation of the Don’t Repeat Yourself principle -- not so much the actual summation of everybody’s age, but the deeper operation: that of iterating across a list and performing some operation on each element in the list, obtaining a result, and combining that with prior results. Abstracting this is a good example of how functional programmers think. We can separate the loop from the details of the operation inside:
let addAges (currentTotal : int) (p : Person) = currentTotal + p.Age let iterateAndSum (people : Person list) = let mutable totalAge = 0 for p in people do totalAge <- addAges totalAge p totalAge
Notice a concise syntax: the last expression in a method determines the return value. Next, the actual “work” done of obtaining the result will need to be varied across each particular usage. The addAges function becomes an argument to a function that can do more than just sum:
let addAges (currentTotal : int) (p : Person) = currentTotal + p.Age let iterateAndOperate (people : Person list) (op: int -> Person -> int) = let mutable totalAge = 0 for p in people do totalAge <- op totalAge p totalAge let totalAge2 = iterateAndOperate people addAges
When the parameter “op” is passed in, it provides the behavior to execute to add the ages together (in the above example). The declaration for “op” is a bit strange, owing to F#’s history, but should be read as “a function taking an int and a Person a yielding an int result”. We now have an example of a higher-order function, because it takes a function to execute as part of its execution—iterateAndOperate takes the “op” function (“addAges” in the above case) as the operation to perform as necessary during the execution of iterateAndOperate.
With this, we can perform any cumulative integer operation on a Person. But why should this operate only on Persons? and only output an integer? If the iterateAndOperate function is written to take any generic type, as well as any appropriate starting value, it looks like:
let iterateAndOperate (seed : 'b) (coll : 'a list) (op : ('b -> 'a -> 'b)) = let mutable total = seed for it in coll do total <- op total it total let totalAge2 = iterateAndOperate 0 people addAges
Several things are happening here. First, the explicit declarations of “int” and “Person” have been replaced bytype parameters “’a” and “’b”, which make this function a generic function, able to take a list of any type “a” and iterate-and-operate on it to produce a single value of type “b”. Then, because the starting value, which was 0 in the earlier form, can’t always be assumed to be 0 (particularly since the ‘b type may not even be numeric), that gets passed in as well.
In itself, this hardly seems remarkable. After all, most business programming doesn’t really involve adding up numbers like this, or if it does, it uses Excel to do it. Most business programming does things far more difficult, like transforming a collection of model objects into XML for transmission across the wire as part of a Web services call, for example.
…which, as it turns out, is exactly what iterateAndOperate is capable of doing, as well:
let personToXML (currentXML : string) (p : Person) = currentXML + "<person>" + p.FirstName + "</person>" let peopleXML = (iterateAndOperate "<people>" people personToXML) + "</people>" System.Console.WriteLine(peopleXML)
…or, as it is more commonly written, passing in an anonymous function at the point of call:
let peopleXML2 = (iterateAndOperate "<people>" people (fun (curr : string) (p : Person) -> curr + "<person>" + p.FirstName + "</person>")) + "</people>" System.Console.WriteLine(peopleXML2)
Note the use of the “fun” keyword (leading many F# speakers to note how F# has put the “fun” back into programming) to define a function inline, which can be useful in a number of cases where the function being defined will only be used once. Naturally, if a particular snippet of code is in fact written more than once, it can (and should) be given a name and reused across several points of use, rather than defined multiple times.
This particular example is so common that it has its own built-in function. “fold” does exactly what the iterateAndOperate function defined above does:
let peopleXML3 = (List.fold (fun (curr) (p : Person) -> curr + "<person>" + p.FirstName + "</person>") "<people>" people) + "</people>" System.Console.WriteLine(peopleXML3)
There are many other things that can be done with functions besides passing them as arguments. If this sounds fun to you, look up partial application and currying.
Summary
In this article, F#’s class definitions, branching and decision-making, pattern-matching, and higher-order function capability have seen the stage, but many other features remain as yet unrevealed. It is the hope of this author that there has been enough here to entice the curious (and the unwary) into spending more time investigating those depths.
F# is a language that embraces multiple language design paradigms within its borders, and attempts to pigeonhole it as a solely-functional or solely-object language will blind potential users to much of its power. Anything that can be done in C# can be done in F#, and work done in F# can be called from C#. The concise syntax is appealing, and great power lies in the function-manipulation capabilities available nowhere else in .NET. While it is certainly possible to use F# to write object-only programs, and while that may be a good path by which to get started learning F#, the greatest benefit will arise from using it the way it was intended: as a mix of the best of both the object-oriented and functional programming worlds.
Jessica Kerr is a consultant with Daugherty Business Solutions in St. Louis, MO. She forges programming and editing skills into a blade which impales bugs and slices run-on sentences into small pieces.
Ted Neward is an Architectural Consultant with Neudesic LLC, the co-author of “Professional F# 2.0” (Wrox), and believes that “he who knows the most programming languages, wins”. Or something like that. Visit his website at http://www.tedneward.com for more details on his activities, or read his blog athttp://blogs.tedneward.com. He resides in Redmond, WA, with his wife, two sons, two cats, one dog, and eight laptops.
评论