Lets focus on filterByPattern
.
A Predicate is a function returning a boolean ('T -> bool
). Since the predicate host most of the logic, lets start by spliting the Predicate from the filter to work on it.
let patternPredicate (includePattern: string Option) (excludePattern: string Option) (path: string) :bool =
let shouldKeep =
match includePattern with
| Some pattern -> matchWildcard pattern path
| None -> true
if not shouldKeep then
false
else
match excludePattern with
| Some pattern -> matchWildcard pattern path |> not
| None -> true
let filterByPattern (includePattern: string Option) (excludePattern: string Option) (paths: string seq) :string seq =
Seq.filter (patternPredicate includePattern excludePattern) paths
Alright now we will stop modifying filterByPattern and focus on patternPredicate. Since we are only dealing with boolean maybe there is a refactoring to do here. Lets switch from pattern matching to basic boolean logic.
let patternPredicate (includePattern: string Option) (excludePattern: string Option) (path: string) :bool =
let shouldKeep = includePattern.IsNone || (includePattern.IsSome && matchWildcard includePattern.Value path)
if not shouldKeep then
false
else
excludePattern.IsNone || (excludePattern.IsSome && (not (matchWildcard excludePattern.Value path))))
shouldKeep
can be seen as (not a) or (a and b)
where a is the option state and b the result of the matching.
Since we don't have to check twice the value of a, (not a) or (a and b)
can be simplify to (not a) or b
. If you want to verify this logic, you can use a Truth Table or rely on a tool like decode.fr's Boolean Expressions Calculator

Lets apply this to our function!
let patternPredicate (includePattern: string Option) (excludePattern: string Option) (path: string) :bool =
let shouldKeep = includePattern.IsNone || (matchWildcard includePattern.Value path)
if not shouldKeep then
false
else
excludePattern.IsNone || (not (matchWildcard excludePattern.Value path))
Thats better! Now lets get rid of this if/else
clause and some parenthesis also.
let patternPredicate (includePattern: string Option) (excludePattern: string Option) (path: string) :bool =
(includePattern.IsNone || matchWildcard includePattern.Value path) &&
(excludePattern.IsNone || not (matchWildcard excludePattern.Value path))
So the complete snippet is now:
let stringToSpan (s: string) =
System.ReadOnlySpan<char>(s.ToCharArray())
let matchWildcard pattern text =
System.IO.Enumeration.FileSystemName.MatchesSimpleExpression(stringToSpan pattern, stringToSpan text)
let patternPredicate (includePattern: string Option) (excludePattern: string Option) path =
(includePattern.IsNone || matchWildcard includePattern.Value path) &&
(excludePattern.IsNone || not (matchWildcard excludePattern.Value path))
let filterByPattern (includePattern: string Option) (excludePattern: string Option) (paths: string seq) :string seq =
Seq.filter (patternPredicate includePattern excludePattern) paths
seq {"test"; "foo/bar"; "foo/toto"}
|> filterByPattern (Some "foo/*") (Some "*toto*")
|> printfn "%A"
// output: foo/bar
.ToCharArray()
in yourstringToSpan
function. You're forcing it to allocate a character array on the heap when it's not needed. If there's nothing built into F# to support this, you should callSystem.MemoryExtensions.AsSpan(s)
in this function instead. \$\endgroup\$