My son likes mazes. He asked me for a webpage that does mazes... Trouble is, maze generation is way too complicated for my
javascript skills. But I was sure I could do better in a more idiot-proof language like F#... Fortunately, now that F# can be
compiled to javascript by the wonderful Fable, maze building in the browser is (just about) achievable for my
simple mind. The demo above has been built entirely from the F#code in this webpage, using
Fable. Graphics are courtesy of Fable's bindings to ThreeJs. (You can also view the
complete code on my GitHub repo
if you prefer). As a disclaimer, nicer and shorter maze generation algorithms are available than the one described here, but this
is what I ended up with and it does illustrate quite a few F# concepts...
Building blocks
So we're going to need to generate a maze, and then render it graphically. My 3D graphics skills are pretty much limited to drawing
cubes, so we'll base our representation of a maze
around single cells, that either do or don't get drawn as a cube. If a Cell is drawn as a cube, it's part of a wall in the maze.
If a Cell is not drawn, then it's a gap or passage in the maze. So a Cell is effectively a thing that can exist as one of several
possible forms (but an individual Cell can only ever be one of these possible forms). In F#, a
discriminated union type fits the bill when we need to encompass
several independent possibilities within the same type. I'll use X to represent a Cell that is part of a maze-wall and O to
represent a Cell that is open-space in the maze. I'll also throw in I to represent a Cell who's type is either as yet indeterminate
or not important (more on that later when we generate mazes).
#r"../../../packages/Fable.Core/lib/netstandard1.6/Fable.Core.dll"#load"../../../node_modules/fable-import-three/Fable.Import.Three.fs"openSystemopenFable.CoreopenFable.Core.JsInteropopenFable.ImportopenFable.Import.Three// Here's our union-type for representing a maze.typeCell=
| X// X is going to represent a wall.
| O// O is going to represent open space / gap / a passage.
| I// I represents intederminate / unknown / don't care.leto=O// Sneaky trick to get syntax-highlighting to show the differenceletx=X// betweeen different cell categories more noticably sometimes.
Bigger bits and pieces
Now that we have established the Cell union type for representing the lowest level building blocks of our maze, we
need to move up a level. The simpest possible maze is a 3 x 3 grid of Cells. We'll call this a SmallSquare. I've
used a tuple with 9 members to represent a SmallSquare. Arguably
I could have attempted to represent the 3x3-ness better using a 2d array or a 3-tuple of 3-tuples etc, but they
would generally introduce more syntactic noise (which F# does a pretty good job of minimizing) in the form of additional
brackets etc, so I'll stick with a tuple. Although the syntax for creating a tuple is pretty minimal, the code will be
clearer (in our case of needing to represent 2-dimensional mazes) if we can omit tuple-syntax (brackets and commas) from
the source. What we can do is create ss which is a curried functionthat requires no commas or brackets around its arguments for creating a SmallSquare. When we are generating mazes,
we will want to replace a 3 x 3 SmallSquare with an equivalent (but more complex) 5 x 5 grid of Cells, which we'll
call LargeSquare. I've also added ls for creating a LargeSquare in a syntactically-minimal way (like ss).
// SmallSquare is a 3x3 grid of CellstypeSmallSquare=Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell// ss gives us a nice-looking way to create a small square// without needing to type lots, of, commas.letssabcdefghi:SmallSquare=a, b, c,
d, e, f,
g, h, i// LargeSquare is a 5x5 grid of CellstypeLargeSquare=Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell*Cell// ls creates LargeSquares in a visually pleasing (i.e. comma-free) way. letlsabcdefghijklmnopqrstuvwxy:LargeSquare=a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t,
u, v, w, x, y
Flipping functions
When we're generating mazes, we'll want to be able to come up with new squares, based on existing ones.
One of the ways we can derive a new square from an existing one is to flip or rotate the existing
square. We can achieve this pretty easily by taking each Cell from a LargeSquare and rearranging
the cells as required. It's reasonably easy to see just by looking at the code how the flip or rotation
transforms the input square's cells. Whilst we're writing utility functions, we'll also include one to
convert the cells from a large square into F# list
form, which will be useful later. As you can see, the F# syntax for creating a list is pretty neat.
// Flip a large square horizontallyletflipabcdefghijklmnopqrstuvwxy=lsedcbajihgfonmlktsrqpyxwvu// Rotate a large square clockwise by 90 degreesletrotateabcdefghijklmnopqrstuvwxy=lsupkfavqlgbwrmhcxsnidytoje// Convert a large square to list representation lettoListabcdefghijklmnopqrstuvwxy= [a; b; c; d; e;
f; g; h; i; j;
k; l; m; n; o;
p; q; r; s; t;
u; v; w; x; y]
Adapting as necessary
Although the above functions are (in my eyes) quite elegant, we can't actually use them at the moment,
as they take each cell individually from a square as input, but we have defined SmallSquare and
LargeSquare as tuples, which group up 9 or 25 Cells into singular entities. To be able to rotate or
flip a square, we will need adapter function that takes a nice-syntax function and an ugly-tuple-syntax
square then extracts each Cell from the square and then passes each Cell individually to the nice-syntax
function. As we will need to use these adapter functions an aweful lot, we'll make them
custom infix operators so
that we can use them in an intuitive way (well, intuitive if you like that kind of thing).
// Adapt a 'nice-syntax' function (sf) to work with an actual SmallSquare.let (|>>|) sf (a, b, c,
d, e, f,
g, h, i) =sfabcdefghi// Adapt a 'nice-syntax' function (sf) to work with an actual LargeSquare.let (||>>||) sf (a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t,
u, v, w, x, y) =sfabcdefghijklmnopqrstuvwxy
All things being equal
We need to be able to work out if a Cell is the same as another Cell. F# helps us out here by
implementing structural-equality
by default on tuple, union and record types. However, if you remember back to the initial definition
of the Cell type, we included I type cells to represent cases where the type of a cell can be
indeterminate (i.e. when it doesn't matter if the Cell finally becomes an X or an O). So, we
need to define equals (|=|) and not-equals (|<>|) operators for Cell type that take the
unimportance of I cells into account. We can also scale equals and not-equals up to work on
LargeSqaures. To do this we convert the squares in question to Lists (via our ||>>|| adapter
function / operator) and use the
List.fold2
function to compare cells from corresponding positions in each square's list and combine the results into a single bool.
// Equals operator for Cellslet (|=|) (a:Cell) (b:Cell) =matcha, bwith
| X, I->true
| O, I->true
| I, X->true
| I, O->true
| _, _ ->a=b// Not-Equals operator for Cellslet (|<>|) (a:Cell) (b:Cell) =matcha, bwith
| I, _ ->false
| _, I->false
| _, _ ->a<>b// Equals operator for LargeSquareslet (||=||) (a:LargeSquare) (b:LargeSquare) =letal=toList||>>||aletbl=toList||>>||bList.fold2 (funsxy->s&& (x|=|y)) truealbl// Not-Equals operator for LargeSquareslet (||<>||) (a:LargeSquare) (b:LargeSquare) =letal=toList||>>||aletbl=toList||>>||bList.fold2 (funsxy->s|| (x|<>|y)) falsealbl
I'll tell you what I don't want
To generate mazes, we need a collection of all possible maze squares. We'll approach this by
generating all possible combinations of cells and then filtering out ones which aren't valid
for some reason (such as ones that contain loops or don't have entry/exit points). To avoid
having to manually specify hundreds of invalid squares, we can write an allRotationsAndFlips
function that takes an square and generates all possible rotated and flipped versions
of it (the idea being that if a square is invalid in one orientation then it is invalid in all
orientations). Our allRotationsAndFlips function accepts a LargeSquare to work on, but it
would also be nice if it accepted individual cells that would form a square, so all is an
adapter function for that. We can use all to help build up a neverValid list of squares that
are not valid in any maze that we generate.
letallRotationsAndFlips (s:LargeSquare) =letr0=sletr90=rotate||>>||r0letr180=rotate||>>||r90letr270=rotate||>>||r180letf0=flip||>>||sletf90=rotate||>>||f0letf180=rotate||>>||f90letf270=rotate||>>||f180
[r0; r90; r180; r270; f0; f90; f180; f270]
// adapter function for allRotationsAndFlipsletallabcdefghijklmnopqrstuvwxy=allRotationsAndFlips<|lsabcdefghijklmnopqrstuvwxy// Using the ampersand operator for list.append is a problem// for my blogging framework (this is a work-around). let (|.|) xy=List.appendxy// Specifies characteristics that make squares invalidletneverValid= [lsIIIIIIoooIIoIoIIoooIIIIII ] // A loop isn't valid|.|allIIxIIIIxIIxxxIIIIIIIIIIII// Small closed section isn't valid|.|allIIIIIIIIIIxxxxxIIIIIIIIII// Medium closed section isn't valid|.|
[lsxxxxxxIIIxxIIIxxIIIxxxxxx ] // Full closed square isn't valid|.|allXoXoXIIIIIIIIIIIIIIIIIIII// Two entry points on any side aren't valid// In a 5x5 cell - corners and certain dividing cells are always required.letalwaysRequired=lsXIXIXIoIoIXIXIXIoIoIXIXIX// Standard structure boundary walls
Give me everything
To actually generate all valid squares, we can write a recursive
function (als) that builds all possible squares (in list form) by starting with an empty list and adding both an X and O
to the empty list and then (recursively) to each list that has already been generated. Rather than generate (5x5)^2 candidate squares,
we note that in a valid (orthodox) square certain wall-cells must always be present in each corner, in the center and in the mid-point
of each side. Similarly, certain cells always have to be open. This means that we only need to generate 12^2 candidates as less
than half the cells are actually variable within a large square. The orthodoxLS function enforces this principal - when given
the variable parts of a square as inputs. We build allLargeSquares by filtering 12^2 possible candidates and filtering out
ones matching neverValid squares with undesirable characteristics. We also put the squares into a set to eliminate any duplicates,
again this is helped by F# implementing structural-equality
by default (rather than using reference equality by default - which is one of the inherent flaws in C#, Java, Javascript...
Kotlin looks better on that but not totally great either!).
When we generate a maze, we will start off with a simple 3x3 Cell maze and generate an equivalent
(in terms of ways in and out of it) 5x5 Cell maze. Then, after that, we'll break the 5x5 maze down
into 4 overlapping 3x3 mazes and then generate equivalent 5x5 mazes for each 3x3 maze (and so on...). This
means that we need to be able to judge if a 5x5 Cell maze is equivalent to a 3x3 Cell maze, which is what
isMatch does for us. The other factor that comes into play is that in larger mazes, if we do the 3x3 to
5x5 replacement from left to right and from top to bottom, then we must take into account that the edges of
preceeding squares place requirements on the edges of squares that come after them (i.e. squares must match
where they meet each other). For example, the left side of a square must match the right side of the preceeding
square. I've called these constraints a SideReq, which come in two varieties; TopReq and LeftReq.
Additionally, if a square is on the top row or the left column of a maze, it doesn't have any constraints
on it's sides from neighbouring squares, so requirements are an option (see Scott Wlaschin's excellent page on
the F# option type for more details. Whilst
you're looking you could also check out his notes on F# record types
which I've used to wrap up a TopReq and a LeftReq as a pair of Reqs).
typeSideReq=
| TopReqof (Cell*Cell*Cell*Cell*Cell) option
| LeftReqof (Cell*Cell*Cell*Cell*Cell) optiontypeReqs= { Top:SideReq; Left:SideReq}
// Is a more-complex 5x5 Cell LargeSquare equivalent (in terms of// ways in and ways out) to a simpler 3x3 Cell SmallSquare (and// does the larger square meet additional requirements imposed by// its neighbours)letisMatchabcdefghijklmnopqrstuvwxya'b'c'd'e'f'g'h'i'
(topReq:SideReq)
(leftReq:SideReq) =letl=lsabcdefghijklmnopqrstuvwxy// Nested function, used to decide if a Cell from a side // of the small square is equivalent to 2 cells from the// same side of the larger squareletsideOKssCelllsCellAlsCellB=ifssCell=XthenlsCellA=X&&lsCellB=XelselsCellA<>lsCellBlettopOK=sideOKb'bdletbottomOK=sideOKh'vxletleftOK=sideOKd'fpletrightOK=sideOKf'jt// Do the cells on the side of a large square meet the // requirements imposed by its neighbourletreqOK (req:SideReq) a''b''c''d''e''=matchreqwith
| TopReqNone
| LeftReqNone->true
| TopReq (Some (a''', b''', c''', d''', e'''))
| LeftReq (Some (a''', b''', c''', d''', e''')) ->a''=a'''&&b''=b'''&&c''=c'''&&d''=d'''&&e''=e'''lettopReqOK=reqOKtopReqabcdeletleftReqOK=reqOKleftReqafkputopOK&&bottomOK&&leftOK&&rightOK&&topReqOK&&leftReqOK
Making a random selection
Each time a maze needs growing a 3x3 SmallSquare needs replacing with an equivalent 5x5
LargeSquare. To do this, we can take our collection of all known large squares - allLargeSquares and
filter is to eliminate ones that don't match (i.e. aren't equivalent to) the small 3x3 square (also
checking that the replacement meets any requirements imposed by its neighbours). It's likely that there
will be more than one candidate replacement square, so replace makes a random choice to ensure that we
generate different mazes each time. The last function in this section, decompose does the work of breaking
a 5x5 CellLargeSquare into a 2x2 arrangement of (partially overlapping) 3x3 CellSmallSquares.
letrandom=newSystem.Random ();
// Get a (randomly selected) LargeSquare that is a valid replacement for// the given small square, and that also meets the given neighbour requirements. letreplaceabcdefghitopReqleftReq=letpossibles=allLargeSquares|>Array.filter (funs-> (isMatch||>>||s) abcdefghitopReqleftReq)
letn=Array.lengthpossiblesletchoice=int (doublen*random.NextDouble ())
Array.itemchoicepossibles// Utility to take the bottom row of a square and turn it into // 'top' requirements for a prospective neighbour below it.letgetBottomAsTopReqabcdefghijklmnopqrstuvwxy=TopReq (Some (u, v, w, x, y))
// Utility to take the right column of a square and turn it into // 'left' requirements for a prospective neighbour to its right.letgetRightAsLeftReqabcdefghijklmnopqrstuvwxy=TopReq (Some (e, j, o, t, y))
// Break a 5x5 LargeSquare down into 4 overlapping 3x3 SmallSquares.letdecomposeabcdefghijklmnopqrstuvwxy= (ssabcfghklm, sscdehijmno,
ssklmpqruvw, ssmnorstwxy)
Growing mazes
The below section has the final steps in this implementation of maze growing. The replaceSquare function takes
a 5x5 LargeSquare, decomposes it into 4 overlapping 3x3 small squares, and then replaces each of those with
5x5 squares. A Maze is defined as a list (rows) of lists of LargeSquares (each individual row also being a list
of squares). This means that each time growMaze is called, it takes each row list of squares and replaces it
with two rows, each of which is twice as long as the input row. Most of growMaze is concerned with extracting
neighbour requirements from rows of squares and passing these requirements into the next row of squares to be
generated. Quite a few list functions are needed, including my personal favourite;
pairwise.
The starting point for generating mazes in this demo is the randomMaze function. It starts with the simplest
possible 3x3 Cell (SmallSquare) maze, replaces it with a random 5x5 (LargeSquare) equivalent and then grows that maze
randomly as many times as requested.
// Replace a large square with 4 equivalent large squares, taking into account// edge requirements from neighbours that have already been similarly replaced.letreplaceSquaresqtopReqLtopReqRleftReqTleftReqB=let (tl, tr,
bl, br) =decompose||>>||sqletntl= (replace|>>|tl) topReqLleftReqTletntr= (replace|>>|tr) topReqR (getRightAsLeftReq||>>||ntl)
letnbl= (replace|>>|bl) (getBottomAsTopReq||>>||ntl) leftReqBletnbr= (replace|>>|br) (getBottomAsTopReq||>>||ntr) (getRightAsLeftReq||>>||nbl)
(ntl, ntr,
nbl, nbr)
// Mazes are list of rows. Each row is a list of the LargeSquares in it.typeMaze=LargeSquarelistlist// Grow a maze by one increment, i.e. replacing each LargeSquare in it with a // 2x2 arrangement of LargeSquares (i.e. growth is quadratic)letgrowMaze (lsll:Maze) =letrecgrloutput (lsll:LargeSquarelistlist) =// Get intra-row requirements so that the new row matches the preceeding new rowletprevRowReqs=matchoutputwith
| [] ->List.init (List.length<|List.item0lsll) (funx-> (TopReqNone, TopReqNone))
| _ ->List.lastoutput|>List.mapi (funib->i, getBottomAsTopReq||>>||b)
|>List.pairwise|>List.filter (fun ((i, r), (j, s)) ->i%2=0) // Only need half of them|>List.map (fun ((i, r), (j, s)) -> (r, s)) // Keep reqs, lose numberingmatchlsllwith
| [] ->output
| row::tail->// Take one row from input// Replace each square in the row with 4 replacements, which means// we replace each row with an new upper row and a new lower row.letfolder ((upper, lower), leftReqT, leftReqB) s (topReqL, topReqR) =let (ntl, ntr,
nbl, nbr) =replaceSquarestopReqLtopReqRleftReqTleftReqBletnewUpperRow, newLowerRow=upper|.| [ntl; ntr], lower|.| [nbl; nbr]
letlReqForNextUpperSquare=getRightAsLeftReq||>>||ntrletlReqForNextLowerSquare=getRightAsLeftReq||>>||nbr
((newUpperRow, newLowerRow), lReqForNextUpperSquare, lReqForNextLowerSquare)
// Replace current row with two rows, using above folder func to take account // of the current input row and the requirements placed on it by the preceeding// new row, generating two new longer rows to replace the one current input row.let (newTop, newBottom), _, _ =List.fold2folder (([],[]), LeftReqNone, LeftReqNone) rowprevRowReqs// Input row has been processed, move it to the outputletnewOutput=output|.| [newTop; newBottom]
grlnewOutputtail// Process next input row (tail)grl [] lsll// Take the simplest possible 3x3 Cell (SmallSquare) maze, replace it with// a random 5x5 (LargeSquare) equivalent (as a single LargeSquare maze) and then// grow that maze randomly as many times as requested. letrecrandomMazen=matchnwith
| nwhenn>1->growMaze (randomMaze (n-1))
| _ -> [[replaceXXXoooXXX
(TopReqNone)
(LeftReqNone)]]
Drawing mazes
I'm not going to go into detail of how the rendering code works, as it's secondary to how the maze is
generated. The approach is basically hierarchical, breaking a Maze back down into rows, the each row
into a list of LargeSquares, and then then breaking each LargeSquare down into rows of Cells. If
a Cell is an X then it is rendered individually as a cube, if it is an O (open space in the maze)
then it is not rendered. The available width and height of the canvas is broken down and allocated to
each row, then square and then cell during rendering to position the elements correctly on screen.
Again I won't dwell on how the graphics setup is done (see my earlier post
here for general details
of how to initialise ThreeJs). Probably worth pointing out that as this involves interacting with a
javascript graphics library so we see lots of mutable properties being accessed (with the <- operator).
Mutable state is generally avoided where possible when writing functional programs, however, when
needed F# can handle (mutability)[https://en.wikibooks.org/wiki/F_Sharp_Programming/Mutable_Data],
to our benefit here. We also create the Easy, Medium, Hard buttons in this block and attach event
handlers to them so that a new maze will be generated when they are clicked. We use the mutibility of the
camera object's properties to implement a zoom-out effect to add some visual interest when the maze is regenerated.
So, the image above shows what you should have seen (if anything went wrong with the actual demo at the top of the page).
You might have noticed that the maze generation algorithm could be better; there's only ever one path between each
quadrant (and sub-quadrant etc...). This makes the maze easier to solve that it might otherwise be, it's a drawback of the
kind of algorithm that I used. Don't worry, many different approaches to maze generation are available - if you can take
some of the above but implement a different kind of maze then I'd be really interested to see the results.
val sf : ('a -> 'b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'h -> 'i -> 'j)
val b : 'b
val c : 'c
val d : 'd
val e : 'e
val f : 'f
val g : 'g
val h : 'h
val i : 'i
val sf : ('a -> 'b -> 'c -> 'd -> 'e -> 'f -> 'g -> 'h -> 'i -> 'j -> 'k -> 'l -> 'm -> 'n -> 'o -> 'p -> 'q -> 'r -> 's -> 't -> 'a1 -> 'a2 -> 'a3 -> 'a4 -> 'a5 -> 'a6)
val j : 'j
val k : 'k
val l : 'l
val m : 'm
val n : 'n
val o : 'o
val p : 'p
val q : 'q
val r : 'r
val s : 's
val t : 't
val u : 'a1
val v : 'a2
val w : 'a3
val x : 'a4
val y : 'a5
val a : LargeSquare
val b : LargeSquare
val al : Cell list
val bl : Cell list
Multiple items module List
from Microsoft.FSharp.Collections
-------------------- type List<'T> = | ( [] ) | ( :: ) of Head: 'T * Tail: 'T list interface IEnumerable interface IEnumerable<'T> member GetSlice : startIndex:int option * endIndex:int option -> 'T list member Head : 'T member IsEmpty : bool member Item : index:int -> 'T with get member Length : int member Tail : 'T list static member Cons : head:'T * tail:'T list -> 'T list static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
val fold2 : folder:('State -> 'T1 -> 'T2 -> 'State) -> state:'State -> list1:'T1 list -> list2:'T2 list -> 'State
Full name: Microsoft.FSharp.Collections.List.fold2
Multiple items type Random = new : unit -> Random + 1 overload member Next : unit -> int + 2 overloads member NextBytes : buffer:byte[] -> unit member NextDouble : unit -> float
Full name: System.Random
-------------------- Random() : unit Random(Seed: int) : unit
type Array = member Clone : unit -> obj member CopyTo : array:Array * index:int -> unit + 1 overload member GetEnumerator : unit -> IEnumerator member GetLength : dimension:int -> int member GetLongLength : dimension:int -> int64 member GetLowerBound : dimension:int -> int member GetUpperBound : dimension:int -> int member GetValue : index:int64 -> obj + 7 overloads member Initialize : unit -> unit member IsFixedSize : bool ...
Full name: 07-03-fsharp-fable-three-maze_.initGraphics
val width : (unit -> float)
val height : (unit -> float)
val scene : 'b
val camera : 'c
val initLights : (unit -> 'd)
val spotLight : obj
val set : elements:seq<'T> -> Set<'T> (requires comparison)
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set
val renderer : 'a
val container : obj
val makeButton : ('d -> int -> 'e -> 'f)
val text : 'd
val difficulty : int
val cssClass : 'e
val button : 'f
val buttonClick : ('g -> obj)
val b : 'g
type Boolean = struct member CompareTo : obj:obj -> int + 1 overload member Equals : obj:obj -> bool + 1 overload member GetHashCode : unit -> int member GetTypeCode : unit -> TypeCode member ToString : unit -> string + 1 overload static val TrueString : string static val FalseString : string static member Parse : value:string -> bool static member TryParse : value:string * result:bool -> bool end
Full name: System.Boolean
type obj = Object
Full name: Microsoft.FSharp.Core.obj
Multiple items type Func<'TResult> = delegate of unit -> 'TResult
Full name: System.Func<_>
-------------------- type Func<'T,'TResult> = delegate of 'T -> 'TResult
Full name: System.Func<_,_>
-------------------- type Func<'T1,'T2,'TResult> = delegate of 'T1 * 'T2 -> 'TResult
Full name: System.Func<_,_,_>
-------------------- type Func<'T1,'T2,'T3,'TResult> = delegate of 'T1 * 'T2 * 'T3 -> 'TResult
Full name: System.Func<_,_,_,_>
-------------------- type Func<'T1,'T2,'T3,'T4,'TResult> = delegate of 'T1 * 'T2 * 'T3 * 'T4 -> 'TResult
Full name: System.Func<_,_,_,_,_>
-------------------- type Func<'T1,'T2,'T3,'T4,'T5,'TResult> = delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 -> 'TResult
Full name: System.Func<_,_,_,_,_,_>
-------------------- type Func<'T1,'T2,'T3,'T4,'T5,'T6,'TResult> = delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 -> 'TResult