An F# program is an assembly made from references to other assemblies and a list of source files. The files are compiled in the order they are listed, and each file may use only what the files before it define. This page describes the three kinds of source file, what each must start with, how the order and the entry point work, and how the text itself is encoded.
The kinds of file
| Extension | Name | What it holds |
|---|---|---|
.fs | implementation file | one or more namespace declaration groups, or one top-level module, or (for a script and the last file of an executable) bare declarations |
.fsi | signature file | the public face of the .fs file with the same name: val and type declarations without bodies |
.fsx, .fsscript | script file | declarations and statements run by F# Interactive; may use #r, #load, #I and #time |
.ml, .mli | OCaml-compatible files | accepted by the compiler on the command line; the offside rule is switched off for them (see below) |
A project file (.fsproj) lists the .fs and .fsi files. It is MSBuild XML and is not F# source; it is covered with the build files.
Implementation files
An implementation file starts in one of three ways.
Namespaces. One or more namespace declarations, each followed by the types and modules it holds. A namespace may be written in several files, and a later namespace line closes the one before it. namespace global places names in the top-level .NET namespace, and namespace rec makes everything in the group mutually recursive.
namespace MyCompany.MyOtherLibrary
type MyType() =
let x = 1
member v.P = x + 2
module MyInnerModule =
let myValue = 1
namespace MyCompany.MyOtherLibrary.Collections
type MyCollection(x: int) =
member v.P = x
A top-level module. A first line of module Name (or module Outer.Inner.Name) makes the whole file one module. The last identifier is the module and the identifiers before it are the namespace, so module MyCompany.MyLibrary.MyModule is the same as a namespace MyCompany.MyLibrary holding a module MyModule. The declarations of a top-level module are not indented. module rec makes the module recursive, and an accessibility word may follow module (module internal Name).
Nothing. A file with neither is anonymous. Only a script and the last file of an executable may be anonymous. Its contents go into a module whose name is made from the file name with the first letter capitalised and the extension removed, so program.fs holds the module Program. If the file name is not a valid identifier the module name is unusable and the compiler warns.
module MyCompany.MyLibrary.MyModule
let x = 1
Compilation order
The checker takes the files one at a time. A signature file is checked against the signatures before it and adds its signature to the environment. An implementation file is checked against the environment so far; if a matching signature file exists, the implementation is also checked against it, and later files see only the signature. If there is no signature file, later files see the implementation.
- The signature file of an implementation must come before it, and every signature file needs an implementation file after it.
- A script may not have a signature file.
- A file may use only what is defined in the files before it (or in the same file, higher up, or in a
recgroup). Circular references between files are not possible. - In a script, each
#loadputs the loaded files before the script, in the order written, and a file loaded twice is placed once, at its first position.
Signature files
A signature file has the same name as its implementation with .fsi in place of .fs, and begins the same way (a namespace, or a module line, or nothing). It repeats the declarations with their signatures only, so it can make parts of the implementation private to the file. The compiler can write one for you with --sig.
namespace Library
type Point =
new: x: float * y: float -> Point
member X: float
member Y: float
member Distance: other: Point -> float
module Geometry =
val origin: Point
val inline square: x: float -> float
val for values and functions, type for types with their member signatures.The rules the documentation gives for signatures:
- A record or union must show all of its fields or cases, or none, in the order of the implementation. A class may show some, all or none of its members.
- A class or struct with constructors must show its base type and all its abstract members and interfaces. An interface must show all its members.
- The accessibility words,
inlineandmutableof a value must match the implementation; so must the number of generic parameters and their constraints,[<Literal>]and its value, and the arity (the pattern of parameters). - When it cannot tell whether a type is sealed or an interface, the compiler needs
[<Sealed>]or[<Interface>]in the signature, and the attributes must agree with the implementation. - A type abbreviation in the implementation must not match a type without an abbreviation in the signature.
- From F# 11 the compiler-semantic attributes of the implementation (
NoDynamicInvocation,RequireQualifiedAccess,AbstractClass,Sealed,CLIMutableand others the documentation lists) must be repeated in the signature; leaving one out is error FS3888 instead of a warning.
Script files
A script is processed like an implementation file with these differences: the side effects of all scripts run at start-up; FSharp.Compiler.Interactive.Settings is opened; #nowarn turns a warning off for the whole compilation; and the directives #r, #I, #load, #time, #help and #quit are allowed. The compiler defines COMPILED and F# Interactive defines INTERACTIVE. A script may begin with a #! line, which is read as a comment (see lexical structure). The directives are on the directives page.
#!/usr/bin/env dotnet fsi
#r "nuget: Newtonsoft.Json, 13.0.3"
#load "helpers.fsx"
open Helpers
printfn "%s" (greet "world")
The entry point
A program has an implicit entry point unless it marks one. With the implicit entry point the static initialiser of the last file on the command line is the program: its top-level code runs from the first line to the last, and exit sets the exit code. With an explicit entry point, a function marked [<EntryPoint>] must be the last declaration of the last file; it has type string array -> int, receives the arguments without the program name and returns the exit code.
[<EntryPoint>]
let main argv =
printfn "Arguments passed to function : %A" argv
0
Static initialisation
Each implementation file, script and interactive fragment has a static initialiser. For an executable with an explicit entry point, the initialiser of the last file is forced as the first thing the entry point does; with an implicit entry point it is the body of that entry point. For scripts it runs at once. For any other file it runs the first time a value with observable initialisation is used, or a member of a type with a static let or static do. Functions, literals, generic values, and non-mutable values bound to a constant expression do not have observable initialisation. The initialiser runs once, under a lock.
Encoding, line ends and indentation
- Files are read as UTF-8 text (the specification says all input is currently assumed to be UTF-8).
- A line ends with
\nor\r\n; the lexer defines no other newline. - Blocks are made by indentation (light syntax, always on). A tab character in code is an error in the current lexer (
lex.fslreports it wherever tab white space appears outside a string or comment); use spaces. Tabs inside strings and comments are fine. - The specification describes
#indent "off"as the first declaration of a file that switches light syntax off (blocks then needbegin,end,doneandin, and tabs are white space). The current lexer reads#indent "on",#light "on"and a bare#lightline and ignores them, and reports#indent "off"and#light "off"as no longer supported. So verbose syntax withbeginandendstill works inside light syntax, but a whole file cannot be switched to it. - The specification says the
.mland.mliextensions switch light syntax off implicitly; that was not checked in the compiler. The ML-compatible forms (e1 or e2ande1 & e2as boolean operators, the wordsmod,sigand the OCaml operator words) are described on the lexical structure page.
Not described
Known, not yet covered
- Reference: the grammar of signature files in full (nested module signatures,
exceptionand extern signatures, member signatures with optional and named arguments), and the full offside rule of light syntax (contexts, the tokens the filter inserts); the specification'slexical-filteringchapter andLexFilter.fswere not read in full. - Reference: the
#referenceand#Includeforms of#rand#Iare in the specification but were not found in the lexer or the current documentation, so they are not shown as supported. - Reference: the file ordering rules of a project (
Compile Includeitems,--parallelcompilation, the F# 11 behaviour of signature-first compilation) are not described; they belong to the project file page.
Unknown
- Reference: whether a byte-order mark is accepted at the start of a file was not checked in the sources (the compiler's file reader was not downloaded).
- Reference: the examples were not compiled; the signature example above was written for this page from the rules, not taken from the compiler.
See also
- Modules, namespaces and access – the declarations inside a module and who can see them
- Directives and conditional compilation –
#if,#nowarn,#rand#load - Overview – the kinds of file and where each is described