Introduction
Lists and sequences are powerful collection types that are
the workhorses of F# development. In this chapter, we will look at lists and
sequences, study the breadth of their functionality, and see how they can be best
applied.
Lists
An F# list, defined in Microsoft.FSharp.Collections.List, is a set of “things”
that exhibits the following characteristics:
·
A list is ordered.
·
A list is immutable – both the list itself and the items in
the list.
·
All the elements of a list are of the same type.
Under the covers, F# implements a list via as a singly linked
list that consists of data nodes allocated in a chain, where the first node
(the head) contains some data and may refer to a “next” or downstream
node. We make a distinction between the first node in the list (the head) and
the “rest of the list”, collective referred to as “the tail.”
A linked list is a recursive data structure that terminates
when the last node in the chain points to a sentinel value such as null
or, in F#’s case, the empty list [] (see below). Before
we can use a list, we need to create one, so let’s get to it.
Creating Lists
The very simplest list you can create or use is the empty
list, represented by empty square brackets:
let emptyList = []
// the empty list, head = tail = []
// by itself, the empty list is of type 'T
// once it contains an element, the list is of a fixed type
To create a list that initially contains some elements, you can
enumerate the individual elements explicitly separated by semicolons, as shown
here:
let fruit = ["apple";
"banana"; "orange"]
// list with 3 string elements (head = apple,
tail = orange)
Note that the elements of a list appear between square
braces and are separated by a semicolon (not a comma), and they
must all be of the same type, otherwise, F# will emit a compiler error, as in
the following example:
let stuff = ["apple";
100]
// error, 100 is not a string
The more common and pragmatic way to construct lists is to
use list comprehensions.
List Comprehensions
List comprehensions are a succinct, powerful way to
construct new lists. F# has two forms of list comprehension syntax: ranges
and generators.
Ranges
Ranges take the following form:
[first_element..step..last_element]
Where the first_element is the
first element in the list, step, which is optional, is the “count
by” parameter, and the last_element is the final value. The first_element
and last_element
are inclusive, meaning they both appear in the list that F# constructs. Let’s
take a look at a few examples:
let
alphabet = ['A'..'Z']
let
digits = [0..9]
let
evens = [0..2..20]
let mul3 = [3..3..99]
To count down, e.g., from 10 to
0,
you need to specify a negative step, as in the following example:
let countdown = [10..-1..0] //
blastoff!
Generators
Generators are another form of list comprehension used
extensively in functional programming. List comprehensions that use generators
take the following form:
[for <identifier> in collection -> expr] or
[for <identifier> in collection do ... yield expr][2]
We will use both forms of list comprehensions in this text. Let break down each
one in turn. The first form of list comprehension uses a lambda function to
generate the list elements. In the following example, we create a list
comprehension that generates the squares of the numbers from 1-10:
let squares = [for i in 1..10 -> i * i]
// squares of 1-10
As you can see here, the square of every element from 1-10
is generated correctly. Suppose, however, we only want the squares of the even
numbers from 1-10. The syntax provided in this form of the
generator is insufficient to support selective element generation. This is
where the second form of list comprehension comes into play.
let evensq = [for i in 1..10 do if i % 2 = 0 then yield i * i] // even
squares
Of course, if it suits you, you can always use the do…yield
form of the list comprehension, sans the filtering expression:
let squares = [for i in 1..10 do yield i * i] // we can
always use this form
It is also possible for a list comprehension to generate
multiple dimensions, using two or more for loops:
let
pairs = [for a in
1..3 do
for b in 2..5 do
yield (a, b)]
yield and yield!
With list comprehensions, we first encounter the keyword yield.
The keyword yield is used to generate a single element
each time it is called. We saw examples of this above.
There is another form of yield, denoted yield!
(pronounced yield bang) that is used to generate a collection of
elements in the form of a list or sequence. Here is a simple example:
[for a in 1 .. 5 do
yield! [ a .. a + 3 ] ]
val it : int list
= [1; 2; 3; 4; 2; 3; 4; 5; 3; 4; 5; 6; 4; 5; 6; 7; 5; 6; 7; 8]
This list comprehension loops over the integers from 1-5,
and for each integer the yield! expression will generate a
collection of elements. When you’re working with arrays, lists, and sequences,
you will sometimes use the yield! expression.
More on List Construction
We’ve looked at constructing an empty list, creating lists
explicitly, and using list comprehensions to generate the lists’ elements.
There are still several other ways we create lists: using the cons operator (::),
concatenating two lists together using the list concatenation operator (@)
and using recursion.
cons (::)
To add a new element to the head of a list, we use the right-associative cons
operator (::). The list we start with may be empty or non-empty.
Let’s take a look at an example:
let
basket0 = []
let
basket1 = "fruit" :: []
let
basket2 = "banana" :: basket1
let
basket3 = "orange" :: basket2
let basket4 = "apples"
:: "peaches" :: "pears" :: []
It may be obvious to the careful reader that creating a list
explicitly, e.g., ["apple"; "banana"; "orange"],
is a shorthand for the cons operator equivalent “apple” :: “banana” ::
“pears”::[]. The cons operator is often used in recursive algorithms to build
up a resulting list based on some computation.
Note that the cons operator does not impact an existing list
– lists are immutable. The cons operator returns a new list.
concat (@)
You can also build a new list from two or more existing
lists via using the @ operator to concatenate them together. Given
the definitions above, we can concatenate lists together like this:
let
bigbasket = basket2 @ basket4 //["banana";"fruit";"apples";"peaches";"pears"]
let holiday = basket3 @ basket4 @ ["wine"; "figs";
"fruit cake (yuk!)"]
Creating Lists using Recursion
In F#, we sometimes need to build a list programmatically.
Let’s suppose, for example, we want to convert an array of integers to a list
of integers. Although this is a contrived example, it demonstrates the idea:
let rec arrayToList (a: int array) n =
if (n = a.Length) then
[]
else a.[n] :: arrayToList a (n + 1)
let a =
Array.create 3 5
let newlist = arrayToList a 0
A question for the reader. Is this implementation tail
recursive?
Happily, the F# List and Array
modules supply generic to_array and to_list functions
repsectively, so we don’t need to write our own like we’ve done here. Note that
we will see other ways to recursively build lists when we study pattern
matching.
Using Lists
Now that we know how to create lists, we need to understand
how best to use them. In this next section, we’ll cover some of the more
interesting and useful List functions. Since we have covered many of
the functional concepts, e.g. folding and filtering, when we discussed functions
(Chapter 8), we won’t rehash them here. In addition, since we now know how to
read generic type signatures, we’ll be able to understand the documentation for
the List
functions in all their majesty (OK, that’s a bit much).
Initialization
Yet another way to initialize a list is to use the List.init
function. This function uses a lambda to initialize the list’s elements:
let lst
= List.init 10 (fun n ->
100 * n) // 10 elements: 0-900 by 100s
let lst2 = List.init 5 (fun
n -> "hello"
+ string n) // 5 elements: hello0-4
Checking for the Existence of Elements
Many times it’s necessary to check to see if a list contains
elements or has a positive length. List.isEmpty and List.Length
help us out here:
let
empty = List.isEmpty(lst)
let len = lst2.Length
You may also want to test to see of the list contains at
least one element that meets the criteria of a given predicate:
let doesExist = List.exists(fun
elem -> elem > 500) lst
You can also write test to check if all elements of the
given list satisfy the given predicate:
let allEven = List.forall(fun
elem -> elem % 2 = 0) lst
Accessing Elements
Because we generally process lists recursively, there are
two functions that we use often. List.hd retrieves the
first element in the list, while List.tl retrieves everything
but the head:
let
squares = List.init 5 (fun n -> n * n)
let head
= List.hd(squares) // could also use squares.Head
let tail = List.tl(squares) //
could also use squares.Tail
val squares : int list = [0; 1; 4; 9; 16]
val head : int = 0
val tail : int list = [1; 4; 9; 16]
There is another function nth that retrieves the nth element
from the list. The nth function makes the list look like an array from the
perspective of element retrieval. The list‘s head is considered to be at index 0.
let cubes = List.init 5 (fun n -> n * n *
n)
let eight = List.nth cubes 2
val cubes : int list = [0; 1; 8; 27; 64]
val eight : int = 8
Finding and Filtering
You can find elements in a list by using predicates or by
index. In the following example, we use List.find to find the
first element for which the predicate is true. We then use the List.findIndex
function to return the index of the first element that satisfies the given
predicate.
let
cubes = List.init 5 (fun n -> n * n * n)
let results = List.find(fun
n -> n > 0 && n % 9 = 0) cubes
val results : int = 27
As we saw when we studied arrays, filtering is a common
operation on collections. It should come as no surprise that List offers a filter
function as well. This function returns a new collection containing all the
elements that satisfy the given predicate.
let kids = ["jessica"; "kimberly";
"melissa"]
let youngest = List.filter(fun name -> name
>= "k") kids
Sorting
Lists also give us the ability to sort them in
various ways. The simplest but least powerful is the sort function. This
function uses the natural order of the input arguments, e.g., strings are
alphabetized, numbers are sorted lowest to highest, etc.:
let
planets = ["mars"; "venus"; "mercury";
"earth"]
let alphabetical = List.sort planets
If you need to take control over the sorting, you can use List.sortWith,
which enables you to supply your own lamdba function. F# hands the lambda a
pair of elements at a time (think of them as the left operand and the right
operand) and the lambda returns either -1, 0 or 1 (-1
= left is smaller, 0 = both equal, 1 = right is smaller).
In the following example, we build a list of tuples. Each
tuple contains the name of a planet, and it’s ordinal order from the sun
(Mercury is the closest, followed by Venus, etc.) Our lambda function extracts
the second element from each tuple (using F#’s snd function) and uses
them as the basis of the sort.
let
planets = [("mars",4); ("venus",2); ("mercury",1);
("earth",3)]
let distanceFromSun =
List.sortWith(fun x y -> if (snd x) <
(snd y) then -1 else
1) planets
Note that quite often, we’ll use the pipeline operator to
feed input into List’s functions. We could have written the
previous example as follows:
let
planets = [("mars",4); ("venus",2); ("mercury",1);
("earth",3)]
let
distanceFromSun =
planets |> List.sortWith(fun x
y -> if (snd
x) < (snd y) then -1 else 1)
Using Aggregate Operators
The List module also provides a family of aggregate
functions. These functions are quite powerful, and enable you to implement
sophisticated processing engines. Note that we saw many of these functions when
we discussed arrays. Let’s take a look at them again from the perspective of
the List
module.
Iteration
The List.iter family of functions iterates through
each element in the list, calling a function for each element visited. Note
that the List.iter functions do not return a value - they simply
enable you to visit each element. The function that’s executed per element must
return unit.
let blastoff = [10..-1..0]
blastoff |> List.iter(printfn "%d")
The other functions in the family include List.iter2,
List.iteri,
and List.iteri2.
Let’s look at an example that uses 2 lists. The lists need to be the same size or
F# will throw an exception.
let
continents =
["n.america"; "s.america"; "europe";
"asia"; "africa";
"australia"; "antarctica"]
let
population =
[524000000L; 382000000L; 731000000L; 4000000000L; 922000000L; 21000000L; 1000L]
List.iter2(fun c p -> printfn "%s has
population %d" c p) continents population
Mapping
Recall that mapping is the process of transforming one set
of elements, the source, into another set of elements, the target. When F#
executes List.map, it visits each element in the source and passes it to a
(lambda) transformation function that we supply. In the following example, we
convert a list of integers representing the ordinal order of planets into their
corresponding names. Note that this is a very contrived example; however, I
want to demonstrate that you can do much more than square numbers:
let
planets = [("mars",4); ("venus",2); ("mercury",1);
("earth",3)]
let
planetOrds = [1; 2; 3; 4]
let
names =
planetOrds
|> List.map (fun po -> fst(List.find(fun
p -> snd p = po) planets))
The first two lines are simple enough. They create the lists
that we’ll use as the basis of our mapping. The next line (which extends over 3
physical lines) runs the List.map function over the planetOrds
list. We can see that planetOrds is piped (|>)
into List.map as a parameter. For each ordinal fed into List.map,
F# executes the supplied lambda function. This lambda function interrogates the
planets
list, looking for a tuple whose second (snd) element equals
that of the current ordinal. When it finds the correct tuple, it returns the
first (fst) element of that tuple, which is the planet’s name. The
target names array thus contains the list of planet names in
ordinal order from the sun.
Folding
As discussed previously, functional languages use folding
and unfolding to process collections. Folding (closely related to reducing) is
the process of distilling a list to a single value, where unfolding is the
process of generating a collection. The List module supports a
family of fold methods, many of which we’ve seen already. The two
primary methods in this family are fold (for folding left)
and foldBack
(for folding right). We present a few examples here for completeness.
// sum numbers 1-10 using a left fold
let
accumulate acc x = acc + x // function
to run (folding function)
let sum = List.fold accumulate 0 [1..10] // folding func, init accumulator, list
Note that in many examples of folding that you will see, the
accumulator function, e.g., accumulate in the previous example, will
be a mathematical operator listed in parentheses. For example, the add operator
looks like this: ( + ) and the multiplication operator like this
( *
). We will cover operators and operator overloading later. For
now, it’s sufficient for you to understand that these operators are (surprise,
surprise) just more functions. They expect 2 parameters, e.g., a + b
or x
* y. To bring this idea home, the following example demonstrates
computing a factorial using List.fold and the mathematical operator ( * )
as the accumulator function:
let
factorial n = [ 1 .. n ] |> List.fold ( * ) 1
// For each integer 1..n, the accumulator =
accumulator * n.
// The accumulator starts off at 1.
let result = factorial 5
// result = 120
The foldBack function enables us to perform right
folds (we process the input list from right to left vs. left to right). Note
that folding a list left vs. folding it right can yield different results
depending on the operator applied, i.e., folding is not commutative.
let nums
= [0..4]
let diff
= List.foldBack(fun element acc -> acc - element) nums 0
// diff = 0 - 4 - 3 - 2 - 1 - 0 = -10
Recall that is you need access to the intermedia value of
the folding operation, you can use List.scan and List.scanBack,
as demonstated in Chapter 8.
Converting Lists
There are times when you’re working with a List,
but need to turn it into a different data structure. This occurs often when you
need to convert a list to an array or a sequence to take advantage of some
functionality the other data structure offers, e.g., when you’re using a
library that needs an array as input, or that you need the rich semantics of a
sequence. The List module has two convenience functions for
converting to arrays and sequences.
·
List.to_array builds an array from the given
collection.
·
List.to_seq builds a new sequence.
Calling these functions is trivial:
let nums = [0..4]
let a = List.to_array nums
let s = List.to_seq nums
Sequences
Sequences, defined in the Microsoft.FSharp.Collections.Seq
module, are conceptually similar to lists. Both data structures are used to
represent an ordered collection of values all of homogenous type. However,
unlike lists, elements in a sequence are computed lazily - as they
are needed, rather than computed all at once. This gives sequences some
interesting properties, such as the ability to represent infinite data
structures.
A sequence is fundamentally an enumerable data structure
that maps to System.Collections.Generic.IEnumerable<T>
under the covers. The bottom line is that when you create a sequence, you
are creating an IEnumerable.
Creating Sequences
When we talk about creating sequences, we generally talk
about “generating sequences”, since the elements are produced on-demand,
a.k.a., generated. Sequences are defined using the following syntax:
seq { expression }
The expression is often referred to as a sequence
expression.
Similar to lists, sequences can be created using range
expressions of various types, e.g., integers, floats, etc.
let s =
{1..10} // val it : seq<int> = seq
[1; 2; 3; 4; ...]
let fs = {1.0..2.0..8.0} // val
it : seq<float> = seq [1.0; 3.0; 5.0; 7.0]
Because sequences are evaluated in a lazy manner, you can
create very large sequences with almost zero penalty. When you create a large
sequence (or any sequence for that matter), what you are really doing is
setting up a collection that can be potentially generated. For example, to
create a sequence with 10 million integers, we we can write the following:
let bs = {1L..1000000L}
This “big sequence” is no more expensive to create than the
the first two in the previous example. The sequence elements are only generated
when needed.
Sequence expressions may involve for,
if
and let
expressions. These types of sequence expressions are akin to the list
comprehensions discussed earlier. The general pattern for a sequence expression
is:
seq { for value in expr .. expr -> expr } or
seq { for value in expr do .. yield expr } or
set { for pattern in seq -> expr }
Let’s look at some examples to illustrate the idea. You should
already be familiar with the first two styles based on list comprehensions:
let s1 = seq { for i in 0..10 -> (i, i
+ i) }
let s2 = seq { for
i in 0..10 do yield (i, i * i) }
let s3 = seq { for
i in 0..10 do if i % 3 = 0 then yield i }
Using a let expression in a sequence expression
enables you to compute intermediary results. A common example is shown below:
let
processInfo =
seq {
for p in
System.Diagnostics.Process.GetProcesses() do
let name = p.ProcessName
let threadcount = p.Threads.Count
yield (name, threadcount) }
The examples above all used the yield keyword to return
a single result. As with lists, you can use the yield! (yield bang) keyword to
return another sequence vs. a single element. The following examples
demonstrates the use of yield! in sequence expressions:
let rec allFiles dir =
seq { for file in
Directory.GetFiles(dir) -> file
for subdir in Directory.GetDirectories dir do yield! (allFiles
subdir) }
In addition to the use of yield! we see here
another common sequence pattern – that of using a secondary iteration (another
for
loop). F# deals with multiple interations in sequences expressions by
generating secondary sequences and concatenating the results to the previous
ones.
The last form of sequence expression uses a pattern.
A pattern, which we will cover in detail in Chapter 11, is an input with a
known structure. Pattern matching is the act of checking to see if the
structure of the given pattern matches a set of rules. F# understands patterns
and can carry out pattern matching in a variety of contexts. For our immediate
purposes, let’s take a look at a sequence expression that leverages patterns:
let tups
= [(1,2); (3,4); (5,6); (7,8)]
let revtups = seq { for (a,
b) in tups ->
(b, a)}
Here, tups is a list of tuples. Each tuple in
this list has the structure (x, y). In the next line, we ask F# to match up the
structure (a, b) with the structure (x,y), e.g., (1,2)
would match and set a=1 and b=2, the values from the
tups
element. If the patterns match, as they do here, F# assigns the first element, a,
to the first element of the tuple, and the second element, b,
to the second element of the tuple. It then yields (->)
a new tuple which contains the values in reverse order. The output from the F#
Interactive Console is:
val tups : (int * int) list = [(1, 2); (3, 4); (5, 6); (7,
8)]
val revtups : seq<int * int>
Because revtups is a sequence, the values aren’t
generated yet – they simply can be generated. Let’s force F# to generate
the elements of the sequence and output them to the F# Interactive Console:
> revtups;;
val it : seq<int * int> = seq [(2, 1); (4, 3); (6, 5); (8, 7)]
Note that sequences are compatible with List,
Array
or [],
and the other .NET collection types such as System.Collections.Generic.SortedList<T>.
Using Sequences
Like arrays and lists, sequences support iteration,
aggregate operations such as folding, mapping, etc. The functions , e.g., Seq.map,
defined in the Seqe module are exact parallels to those
defined in the List and Array modules;
therefore, it doesn’t make a lot of sense to rehash them here. Instead, let’s
examine some of the new and different functions specific to sequences
themselves. Of course, the functions we discuss here are not exhaustive, but
representative of the more commonly used ones. As with any module, please
consult the documentation for full details.
Seq.delay is a function that creates a sequence
from a function that itself return a sequence. Why on earth would you need
that, since standard sequences are already delay computed? The answer is rooted
in the fact that the creation of certain sequences can cause side effects – and
you may want to delay those side effects. Side effects are prevelant, for
example, when you interact with the .NET libraries. Let’s take a common example
of iterating over the directories and files in a given directory, which we
discussed previously:
let rec allFiles dir =
seq { for file in
Directory.GetFiles(dir) -> file
for subdir in
Directory.GetDirectories dir do yield! (allFiles subdir) }
let
sysfiles = allFiles @"C:\windows\system32"
In this example, the creation of the sequence causes F# to
generate the first set of files from the root directory dir
passed into the function. From Don Syme’s Expert F#:
One subtlety with programming with sequences is that side
effects such as reading and writing from an external store should not in
general happen until the sequence value is actually iterated. In particular,
the allFiles function as specified above reads the top-level
directory C:\ as soon as allFiles is applied
to its argument. This may not be appropriate if the contents of C:\ are
changing. You should delay the implementation of the sequence until iteration
is performed. That is, when data sources may change and you wish to see the
changes in subsequent iterations then a sequence value should be a
“specification” of a how to generate a sequence rather than a single read of
the data source. You can achieve this by using Seq.delay, shown
below.
let rec allFiles dir =
Seq.delay (fun () ->
let files = Directory.GetFiles(dir)
let subdirs = Directory.GetDirectories(dir)
Seq.append
files
(subdirs |> Seq.map allFiles |> Seq.concat))
So, the moral of the story is: when you are interacting with
external data sources that can change out from under you, and you want to make
sure that you get the latest-and-greatest data from these sources via
sequences, consider using Seq.delay.
To provide a bit more insight into what the compiler
generates, we can use Reflector to get a better sense of what’s happening (C#
syntax as produced by Reflector). Here is what the non-delayed function looks
like:
public static IEnumerable<string> allFiles(string dir)
{
return new
allFiles@8(dir, null, null,
null, null, null, null, 0, null);
}
Here is the delayed version:
public static IEnumerable<string>
allFilesDelayed(string dir)
{
return SeqModule.delay<string>(new allFilesDelayed@12(dir));
}
As you can see, the non-delayed version constructs the IEnumerable
immediately, where the delayed version waits to construct the IEnumerable.
If the construction of the IEnumerable produces unwanted side
effects, you have Seq.delay at your disposal.
One of the functions that we did not cover when discussing
Arrays or Lists is concat (used in Don Syme’s example above).
While not the exclusive domain of the Seq module, you will
see it used with sequences somewhat often. With a name like concat,
it’s probably not too surprising that it concatenates its inputs together. It
is often used in conjunction with recursive functions and data structures for
building up a result. concat takes a list of enumerables and returns
a single enumerable. Most of the examples on this topic are somewhat arcane, so
to be clear we’ll create a simple, albeit contrived, example to demonstrate the
idea. For this example, it’s easiest to build a data structure that explicitly
contains a collection of enumerables. We will tap into the .NET libraries for
help.
// Build up a list of string[]. This is our "sequence of
sequences".
open
System.Collections.Generic
let words =
let wordList = new
List<string[]>()
wordList.Add([| "mary"; "had"; "a";
"little"; "lamb"
|])
wordList.Add([| "it's"; "fleece"; "was";
"white"; "as";
"snow..." |])
wordList
// Tell F# to take the sequences and string them together. Take
the final
// sequences and run it through Seq.iter to dump all the words.
let rhyme = words |> Seq.concat |> Seq.iter(fun w -> printf "%s " w)
Note that Seq.concat differs from
a similarly named function Seq.append, the names of which can be
confusing. Whereas concat “strings together” a set of enumerables
into a single enumerable, append takes two sequences and adds one
to the end of the other. The function signatures tell the story:
let
concat_sig = Seq.concat
let append_sig = Seq.append
val concat_sig : (seq<#seq<'b>> ->
seq<'b>)
val append_sig : (seq<'a> -> seq<'a> -> seq<'a>)
What these signatures tell us is that concat
is a function that takes a sequence set and returns a sequence that is
type compatible with the set. The #seq notation has to do
with flexible types, which we’ll cover later. The signature for append,
on the other hand, tells us that this function takes 2 different sequences and returns
a new sequence.
Converting Sequences
When working with sequences, it’s often convenient to turn
them into arrays and lists. To convert a sequence into an array, use the Seq.to_array
function, as shown here:
let arr = seq { 1..10 } |> Seq.to_array
You have the same option to turn the sequence into a list as
well using Seq.to_list:
let lst = seq {1..10} |> Seq.to_list
Well, you know now enough about lists and sequences to
understand many of the samples that you will encounter in the wild. You also
have enough background to make some sense of the F# documentation, and can
probably write some non-trivial F# code. Let’s use this momentum and dive into
pattern matching.
What You Need to Know
·
Lists are immutable data structures that hold an ordered set of
elements, all of the same type. Under the covers, Lists are implemented as singly
linked lists.
·
Lists, like most data structures in functional programming, are
generally processed recursively.
·
Lists have a wide range of built-in operations including folding,
mapping, zipping, etc. You should already have enough F# knowledge now to
understand the documentation for Lists.
·
Lists can be constructed using the cons (::)
operator, the concatenation (@) operator, using
explicit delineation of elements, or via list comprehensions.
·
During iteration/comprehension, yield returns a single
element, while yield! (yield bang) return a collection
of elements.
·
Sequences are enumerable data structures whose elements are
evaluated lazily (on demand). In other words, they are F#’s way to create and
use IEnumerable<T>.
·
You create sequences using the seq { } syntax and a
sequence expression. The expression can list the elements explicitly, use a
compatible collection such as a list, or use an arbitrary function to generate
the elements.
·
Like lists and arrays, sequences support a full complement of
functions including mapping, unfolding, and (unique to sequences) infinite
constructs.
|