Most network-based C libraries refer to socket.h to describe the type of socket that can be used with their API so it’s an important entry point for a lot of network operations and one that would be nice to support as generically as possible in OCaml.
The catch, though, is that, most likely for historical reasons¹, the POSIX specifications only partially defines some of the required data structures and types, which makes it possible to write C code using them but does not give enough information to write C bindings without having to use the compiler to parse the actual system-specific headers of the running host.
For instance, here’s how the sockaddr structure is specified:
-
The <sys/socket.h> header defines the sockaddr structure that includes at least the following members:sa_family_t sa_family address family char sa_data[] socket address (variable-length data)
+
The <sys/socket.h> header defines the sockaddr structure that includes at least the following members:sa_family_t sa_family address family char sa_data[] socket address (variable-length data)
Likewise, here’s what is specified about the size of the socklen_t data type:
-
<sys/socket.h> makes available a type, socklen_t, which is an unsigned opaque integral type of length of at least 32 bits.
+
<sys/socket.h> makes available a type, socklen_t, which is an unsigned opaque integral type of length of at least 32 bits.
Thus, in order to know the exact offset of sa_family inside the sockaddr structure or the actual size of a socklen_t integer, one has to include the OS-specific header, parse its definitions for that specific OS and, only then, is it possible to compute that offset or data size. Let’s see how it’s done in our binding now!
You can also execute a single-line with Alt + ‘. I rarely use this option, but this can save you time because you don’t need to select the entire line of code.
-
In case the keyboard shortcuts to send code to FSI do not work anymore (ReSharper used to over-write them in the past), you can reset them in Visual Studio, by going to Tools / Options / Environment / Keyboard. The 2 commands you need to map are EditorContextMenus.CodeWindow.ExecuteInInteractive and EditorContextMenus.CodeWindow.ExecuteLineInInteractive.
+
In case the keyboard shortcuts to send code to FSI do not work anymore (ReSharper used to over-write them in the past), you can reset them in Visual Studio, by going to Tools / Options / Environment / Keyboard. The 2 commands you need to map are EditorContextMenus.CodeWindow.ExecuteInInteractive and EditorContextMenus.CodeWindow.ExecuteLineInInteractive.
You can also use these shortcuts from a regular .fs file, which can be handy if you want to validate that a piece of code is behaving the way you want.
@@ -297,7 +297,7 @@
Tip 6: Use Paket
The Nuget package manager is useful to consume existing packages. However, by default, Nuget stores assemblies in a folder that includes the package version number. This is very impractical for a script. In our example above, if fsharp.data gets an update, our script reference will be broken once we update the Nuget package:
Fixing the script requires manually editing the version number in the path, which quickly becomes a pain. Paket provides a better experience, because it stores packages without the version number, in this case, under:
+
Fixing the script requires manually editing the version number in the path, which quickly becomes a pain. Paket provides a better experience, because it stores packages without the version number, in this case, under:
What we’re after, are more lightweight structures (less than 1kB), that can live fully in a user space, so that we can have even millions of them cooperating frequently with each other without heavy performance penalties.
Before we begin, I think it’s good to discuss different designs. We’ll cover several different topics to be able to make more informed decisions, that we’re up to apply to our own solution.
Preemptive vs cooperative scheduler
Scheduler is a subsystem, which direct responsibility is to assign CPU core processing power to a particular fiber. It’s also responsible for coordinating fibers execution. The two most common categories of schedulers are preemptive and cooperative.
-
A preemptive scheduler is the one, that’s always in control of fiber execution. It’s able to decide on its own, when fiber can be started and stopped. The most obvious example of such is a thread scheduler existing on most operating systems.
+
A preemptive scheduler is the one, that’s always in control of fiber execution. It’s able to decide on its own, when fiber can be started and stopped. The most obvious example of such is a thread scheduler existing on most operating systems.
Preemptive scheduler usually works in one of two ways:
Time based scheduler takes a quant of CPU time and gives it to a given fiber, which ten can execute its logic until it reaches its execution time limit (of course, it can finish earlier). This is how OS thread scheduler, but also how Go goroutine scheduler works.
Another variant is step-based scheduler, which splits fiber’s function body into series of (more or less equal) steps. Then each fiber is given a number of steps to execute before preemption occurs. Example of such is Erlang’s BEAM - it simply allows each process to execute up to 2000 “reductions”, where each reduction is basically a function call. And since in Erlang there are no loops, only tail-recursive functions, this approach works well for long-living iterative processes as well.
-
One of the problems with preemptive schedulers is that they usually need some kind of involvement from the compiler or hosting virtual machine in order to work. For this reason, most of the fiber libraries use cooperative schedulers to perform their work.
-
A cooperative scheduler doesn’t have a concept of preemption - once started by the scheduler, a fiber will execute until it doesn’t give back the control willingly. This is often done with dedicated programming constructs, and often is known as yielding, parking or awaiting.
+
One of the problems with preemptive schedulers is that they usually need some kind of involvement from the compiler or hosting virtual machine in order to work. For this reason, most of the fiber libraries use cooperative schedulers to perform their work.
+
A cooperative scheduler doesn’t have a concept of preemption - once started by the scheduler, a fiber will execute until it doesn’t give back the control willingly. This is often done with dedicated programming constructs, and often is known as yielding, parking or awaiting.
In cooperative variant, a fiber body is usually split into series of discrete steps, between which fiber gives control back to the scheduler.
Keep in mind that these two are not mutually exclusive - a preemptive scheduler often provides a way for a fiber to return control back to it when it’s known that fiber won’t be executing any longer eg. because it has been put to sleep for a while.
Stackless vs. stackful
A concept, that’s somewhat related to a topic above is the idea of stackless and stackful coroutines.
@@ -221,11 +221,11 @@
Finite state machines - this variant is usually faster and can be encoded manually (example of such case is Akka actors), but for a human eye it usually doesn’t really read as a sequential step-by-step program execution, unless it has some support from the compiler itself (see: C# and Rust).
Monadic sequencing via bind/flatMap operator, which is very popular in functional languages. While we cover it in more details in the rest of this blog post, for now it’s enough to say that it’s a way to chain callback-based behaviors together in a way, that resembles standard sequential code.
-
For sure one of the advantages of stackful coroutines is that they’re mono-colored: you can yield/continue coroutine execution from within any other function, while in the stackless variant splits your world into two-colored functions - synchronous and asynchronous - where async one can be only called and yielded safely (without blocking underlying OS thread) from within another async function.
+
For sure one of the advantages of stackful coroutines is that they’re mono-colored: you can yield/continue coroutine execution from within any other function, while in the stackless variant splits your world into two-colored functions - synchronous and asynchronous - where async one can be only called and yielded safely (without blocking underlying OS thread) from within another async function.
Eager vs lazy fibers
We already mentioned two important events in fiber execution life cycle - starting and parking. Here I briefly discuss about different design decisions on when to start a fiber execution.
-
Eager execution means, that fiber is started automatically after its creation. An example of such are Scala Future[A] and JavaScript Promise. Since execution process starts right away, we’re willingly resign from a certain degree of control over how or when to execute given fiber. Usually this is solved by wrapping a fiber creation into another function or lambda.
-
Lazy execution is much more common and preferred way of work, as it allows us to separate place where we want to define our asynchronous sequence of steps from the place, where the execution details are defined. It’s used in C# TPL as well as pretty much in all functional languages implementations (excluding Scala futures mentioned earlier).
-
Interruption
There are also few decisions regarding premature escaping the fiber execution, also known as interruption/cancelation: one of them requires passing special object - a token - between method calls and explicit checking for its completion. It is how C# Tasks work. However putting such requirement onto the API user can be cumbersome and error-prone option. Therefore pretty much every other coroutine library either allows to direcly interrupt a fiber or (like in case of F# Async) passes cancelation tokens and check if they were triggered under the hood.
+
Eager execution means, that fiber is started automatically after its creation. An example of such are Scala Future[A] and JavaScript Promise. Since execution process starts right away, we’re willingly resign from a certain degree of control over how or when to execute given fiber. Usually this is solved by wrapping a fiber creation into another function or lambda.
+
Lazy execution is much more common and preferred way of work, as it allows us to separate place where we want to define our asynchronous sequence of steps from the place, where the execution details are defined. It’s used in C# TPL as well as pretty much in all functional languages implementations (excluding Scala futures mentioned earlier).
+
Interruption
There are also few decisions regarding premature escaping the fiber execution, also known as interruption/cancelation: one of them requires passing special object - a token - between method calls and explicit checking for its completion. It is how C# Tasks work. However putting such requirement onto the API user can be cumbersome and error-prone option. Therefore pretty much every other coroutine library either allows to direcly interrupt a fiber or (like in case of F# Async) passes cancelation tokens and check if they were triggered under the hood.
Implementation
Since we talked a bit about various approaches, let’s get to the meat of this blog post: implementing our own coroutine library in F#. So, what properties will it have?:
We use cooperative scheduling (we don’t want to tweak the compiler) of stackless fibers with support from F# computation expression for nice syntax.
@@ -268,7 +268,7 @@
Now, since our cancellation is not explicit, we need to deal with few things:
Whenever parent fiber is cancelled, all child fibers it spawned are also cancelled.
-
Whenever we cancel a fiber that loose the race, we don’t want to accidentally cancel a token of its parent.
+
Whenever we cancel a fiber that loose the race, we don’t want to accidentally cancel a token of its parent.
This behavior implies at least using two separate tokens, however in practice it will be more pragmatic to make our Cancel token work as a tree hierarchy - this way we can easily keep track of things and support more complex scenarios.
[<Sealed;AllowNullLiteral>] typeCancel(parent: Cancel) = letmutable flag:int=0 letmutable children: Cancel list= [] new() = Cancel(null) /// Check if token was cancelled member __.Cancelled = flag =1 /// Remove child token memberprivate __.RemoveChild(child) = letrec loop child = let children' = children let nval = children' |> List.filter ((<>) child) ifnot (obj.ReferenceEquals(children', Interlocked.CompareExchange(&children, nval, children'))) then loop child ifnot (List.isEmpty children) then loop child /// Create a new child token and return it. member this.AddChild () = letrec loop child = let children' = children if (obj.ReferenceEquals(children', Interlocked.CompareExchange(&children, child::children', children'))) then child else loop child loop (Cancel this) /// Cancel a token member this.Cancel() = if Interlocked.Exchange(&flag, 1) =0then for child in Interlocked.Exchange(&children, []) do child.Cancel() ifnot (isNull parent) then parent.RemoveChild(this)
@@ -379,7 +379,7 @@
1 2 3 4 5 6 7 8 9
letrec run () = match Seq.tryHead timeline with |None-> running <-false |Some (KeyValue(time, bucket)) -> timeline <- Map.remove time timeline currentTime <- time for fn in List.rev bucket do fn () run ()
We’ll try to pick the first entry from the timeline - since here we use F# map, which is sorted in ascending order, we know that first entry is the one with the shortest execution timeout. We update our “current” time to match the expected one we calculated earlier, and finally we execute all functions scheduled at that time and repeat the loop all over until we eventually run out of scheduled actions.
-
Now here’s the trick - we use List.rev to execute functions in the same order in which they were scheduled, because we want our tests to be deterministic and our bugs to be reproducible. However this is not the only strategy - since we know that functions in the same bucket could as well be executing in parallel, we could shuffle them around in different permutations for early discovery of some data races! I’ll won’t dive into it, but leave that idea as food for thoughts for you.
+
Now here’s the trick - we use List.rev to execute functions in the same order in which they were scheduled, because we want our tests to be deterministic and our bugs to be reproducible. However this is not the only strategy - since we know that functions in the same bucket could as well be executing in parallel, we could shuffle them around in different permutations for early discovery of some data races! I’ll won’t dive into it, but leave that idea as food for thoughts for you.
One last note about the test scheduler is that isolating it from the actual physical clock means, we cannot trust our time functions (like DateTime.UtcNow) any longer. This shouldn’t really be an issue though - because relying on physical time would potentially make our tests indeterministic, we didn’t want to use it anyway, right?
However, we need to be able to obtain current time from the scheduler, so we need to extend its API:
1 2 3 4 5 6 7 8 9 10
typeIScheduler= abstract UtcNow:unit->unit // ... other methods typeTestScheduler() = letmutable currentTime = DateTime.UtcNow.Ticks // ... rest of the implementation interface IScheduler with member __.UtcNow() = DateTime(currentTime) // ... other methods
Maybe aside of the logger we may be needing a separate telemetry mechanism to count number of incoming request or password validation failures? That means another parameter.
Salt generation is pseudo-random process - it we want our function to be deterministic, we should probably parametrize it over explicitly passed Random as well.
-
As you see, what seemed to be simple task at the beginning can quickly blow up out of proportion. As the number of arguments grows, the more nasty our wiring code eventually becomes. Quite common pattern is to hide all of that nastiness under the carpet a.k.a. composition root. However this doesn’t have to be the case.
+
As you see, what seemed to be simple task at the beginning can quickly blow up out of proportion. As the number of arguments grows, the more nasty our wiring code eventually becomes. Quite common pattern is to hide all of that nastiness under the carpet a.k.a. composition root. However this doesn’t have to be the case.
Below we’ll cover another approach for dealing with dependencies - inspired by Scala ZIO library - using incremental steps, from first principles to monadic bindings.
Managing dependencies beyond partial application
Let’s start from how our code from above will eventually look like at the end of this step:
1 2 3 4 5 6 7 8 9 10 11
let changePass env =fun req ->task { let! user = Db.fetchUser env req.UserId if user.Hash = bcrypt user.Salt req.OldPass then let salt = Random.bytes env 32 do! Db.updateUser env { user with Salt = salt; Hash = bcrypt salt req.NewPass } Log.info env "Changed password for user %i" user.Id returnOk () else Log.error env "Password change unauthorized: user %i" user.Id returnError"Old password is invalid" }
@@ -255,8 +255,8 @@
It doesn’t impose specific restrictions on libraries and frameworks.
Now we could as well stop here - IMHO this approach is already good and useful for most cases. We can also try to push it further. As you’ve seen, our code now requires quite a lot of env passing around. Could we do something about this? It turns out that yes, we could.
-
Reader monad
Before we continue: what we’re going to cover now is less useful in terms of current state of F# ecosystem for the reasons I’ll mention later.
-
The pattern we’ll use here is known as a Reader Monad. While it’s useful in certain situations, it’s not widely used - IMO it’s fault lies in the name itself, which somehow managed to sound both borderline meaningless and scary in ears of many developers.
+
Reader monad
Before we continue: what we’re going to cover now is less useful in terms of current state of F# ecosystem for the reasons I’ll mention later.
+
The pattern we’ll use here is known as a Reader Monad. While it’s useful in certain situations, it’s not widely used - IMO it’s fault lies in the name itself, which somehow managed to sound both borderline meaningless and scary in ears of many developers.
The rest of this blog post will be introduction to this style in F#, however focused solely around problem of dependency management - we’ll ignore other aspects of monads.
We’ll going to reuse our environment type from above, but now encode it directly into another type we’ll call Effect. Since I’ve mentioned that our pattern has M-word in it, you can safely assume that our handler’s logic will be defined as a lazy sequence of steps to be executed (sounds almost like async/await). In F# we’ll sugar them by using custom computation expression (I’m going to call it effect { ... }) returning our effect type, which we’ll define as:
1
[<Struct>]typeEffect<'env, 'out>= Effect of ('env->'out)
memberpublic this.UpdateMyName (command: UpdateUsernameCommand) (user: User) = let user = userRepository.GetById user.Id let oldName = user.Username let newName = command.Username
纯函数 vs. 非纯函数 (Pure vs. Impure Functions): Capture Checking 显式地区分了纯函数和非纯函数。类型为 A => B 的函数被认为是非纯函数,它可以捕获任意 Capability,等价于 A ->{cap} B 1。而类型为 A -> B 的函数则是纯函数,它不能捕获任何 Capability。此外,还可以使用 A ->{c, d} B 的形式来显式指定函数只能捕获 Capability c 和 d。这种区分使得类型系统能够强制执行函数式编程的原则,其中纯函数因其可预测性和可测试性而备受推崇。
纯函数 vs. 非纯函数 (Pure vs. Impure Functions): Capture Checking 显式地区分了纯函数和非纯函数。类型为 A => B 的函数被认为是非纯函数,它可以捕获任意 Capability,等价于 A ->{cap} B 1。而类型为 A -> B 的函数则是纯函数,它不能捕获任何 Capability。此外,还可以使用 A ->{c, d} B 的形式来显式指定函数只能捕获 Capability c 和 d。这种区分使得类型系统能够强制执行函数式编程的原则,其中纯函数因其可预测性和可测试性而备受推崇。
defusingLogFile[T](op: FileOutputStream => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() result
@@ -219,47 +219,47 @@
1 2 3 4
| val later = usingLogFile { f => () => f.write(0) } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |The expression's type () => Unit is not allowed to capture the root capability `cap`. |This usually means that a capability persists longer than its allowed lifetime.
在 Scala 3 中,函数类型 A => B 被认为是不纯的,它可以捕获任意 Capability。实际上,A => B 是 A ->{cap} B 的别名,明确地表明了它可能捕获 “通用 Capability”。这种默认行为反映了 Scala 过去函数可以拥有任意副作用的特点。然而,随着 Capture Checking 的引入,开发者被鼓励更明确地表达函数的纯度。
-
与不纯函数相对的是纯函数,其类型为 A -> B,表示该函数不能捕获任何 Capability。纯函数是函数式编程中的核心概念,Capture Checking 提供了一种在类型层面强制执行纯性的方法。确保函数的纯性可以使代码更具可预测性和可测试性,因为纯函数的输出完全取决于其输入,并且没有副作用。
-
开发者还可以指定函数可以捕获的特定 Capability,语法为 A ->{c, d} B,表示该函数可以捕获 Capability c 和 d。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。
-
捕获注解^ 的优先级高于 -> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,A ^ C -> B 表示一个从捕获的 A 到 B 的纯函数。
+
在 Scala 3 中,函数类型 A => B 被认为是不纯的,它可以捕获任意 Capability。实际上,A => B 是 A ->{cap} B 的别名,明确地表明了它可能捕获 “通用 Capability”。这种默认行为反映了 Scala 过去函数可以拥有任意副作用的特点。然而,随着 Capture Checking 的引入,开发者被鼓励更明确地表达函数的纯度。
+
与不纯函数相对的是纯函数,其类型为 A -> B,表示该函数不能捕获任何 Capability。纯函数是函数式编程中的核心概念,Capture Checking 提供了一种在类型层面强制执行纯性的方法。确保函数的纯性可以使代码更具可预测性和可测试性,因为纯函数的输出完全取决于其输入,并且没有副作用。
+
开发者还可以指定函数可以捕获的特定 Capability,语法为 A ->{c, d} B,表示该函数可以捕获 Capability c 和 d。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。
+
捕获注解 ^ 的优先级高于 -> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,A ^ C -> B 表示一个从捕获的 A 到 B 的纯函数。
与函数类型类似,Capture Checking的概念也延伸到了命名参数类型。=> Int 允许任意Capability引用,类似于不纯函数类型。-> Int 禁止任何Capability引用,类似于纯函数类型。而 ->{c} Int 则只允许引用Capability c。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。
与函数类型类似,Capture Checking的概念也延伸到了命名参数类型。=> Int 允许任意Capability引用,类似于不纯函数类型。-> Int 禁止任何Capability引用,类似于纯函数类型。而 ->{c} Int 则只允许引用Capability c。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。
module Log = let info (env: #ILog) = env.Logger.Info("Message")
module Db = let fetchUser (env: #IDb) = env.Database.Query(...)
-
优点:
+
优点:
-
显式依赖声明:函数签名仅需env参数,编译器验证接口实现。
-
模块化隔离:各模块仅声明所需接口(如ILog、IDb),避免全局依赖。
-
易于测试:通过模拟env实现单元测试,无需依赖具体实现。
+
显式依赖声明:函数签名仅需env参数,编译器验证接口实现。
+
模块化隔离:各模块仅声明所需接口(如ILog、IDb),避免全局依赖。
+
易于测试:通过模拟env实现单元测试,无需依赖具体实现。
-
应用场景:
+
应用场景:
1 2 3 4 5
let changePass env req =task { let! user = Db.fetchUser env req.UserId Log.info env "Processing user: %i" user.Id ... }
@@ -226,15 +226,15 @@
然后:
1 2 3 4 5 6
let changePass req =effect { let! user = Db.fetchUser req.UserId let! salt = Random.bytes 32 do! Log.info "Password updated for user %i" user.Id returnOk() }
N+1 查询问题是指在通过 ORM 查询数据时,执行了一次初始查询来获取父对象列表(这 1 次查询),然后为列表中的每一个父对象都单独执行了一次额外的查询来获取其关联的子对象(这 N 次查询)。最终导致总共执行了 1 + N 次数据库查询,其中 N 是初始查询返回的父对象的数量。
-
举个例子:
+
N+1 查询问题是指在通过 ORM 查询数据时,执行了一次初始查询来获取父对象列表(这 1 次查询),然后为列表中的每一个父对象都单独执行了一次额外的查询来获取其关联的子对象(这 N 次查询)。最终导致总共执行了 1 + N 次数据库查询,其中 N 是初始查询返回的父对象的数量。
+
举个例子:
假设有两个数据库模型:User(用户)和 Post(帖子),一个用户可以有多篇帖子(一对多关系)。
现在,需要获取前 10 个用户以及他们各自的所有帖子。
-
一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:
+
一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:
-
第一次查询 (The “1”): 获取前 10 个用户。
1
SELECT*FROMUser LIMIT 10;
-
接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。
1 2 3 4 5 6 7 8
-- 用户 1 SELECT*FROM Post WHERE authorId =1; -- 用户 2 SELECT*FROM Post WHERE authorId =2; -- 用户 3 SELECT*FROM Post WHERE authorId =3; -- ... 直到 用户 10 SELECT*FROM Post WHERE authorId =10;
+
第一次查询 (The “1”): 获取前 10 个用户。
1
SELECT*FROMUser LIMIT 10;
+
接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。
1 2 3 4 5 6 7 8
-- 用户 1 SELECT*FROM Post WHERE authorId =1; -- 用户 2 SELECT*FROM Post WHERE authorId =2; -- 用户 3 SELECT*FROM Post WHERE authorId =3; -- ... 直到 用户 10 SELECT*FROM Post WHERE authorId =10;
-
在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。
+
在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。
You can also execute a single-line with Alt + ‘. I rarely use this option, but this can save you time because you don’t need to select the entire line of code.
-
In case the keyboard shortcuts to send code to FSI do not work anymore (ReSharper used to over-write them in the past), you can reset them in Visual Studio, by going to Tools / Options / Environment / Keyboard. The 2 commands you need to map are EditorContextMenus.CodeWindow.ExecuteInInteractive and EditorContextMenus.CodeWindow.ExecuteLineInInteractive.
+
In case the keyboard shortcuts to send code to FSI do not work anymore (ReSharper used to over-write them in the past), you can reset them in Visual Studio, by going to Tools / Options / Environment / Keyboard. The 2 commands you need to map are EditorContextMenus.CodeWindow.ExecuteInInteractive and EditorContextMenus.CodeWindow.ExecuteLineInInteractive.
You can also use these shortcuts from a regular .fs file, which can be handy if you want to validate that a piece of code is behaving the way you want.
@@ -102,7 +102,7 @@
Tip 6: Use Paket
The Nuget package manager is useful to consume existing packages. However, by default, Nuget stores assemblies in a folder that includes the package version number. This is very impractical for a script. In our example above, if fsharp.data gets an update, our script reference will be broken once we update the Nuget package:
Fixing the script requires manually editing the version number in the path, which quickly becomes a pain. Paket provides a better experience, because it stores packages without the version number, in this case, under:
+
Fixing the script requires manually editing the version number in the path, which quickly becomes a pain. Paket provides a better experience, because it stores packages without the version number, in this case, under:
mess with your coworkers’ mental sanity, by executing (* (opening a multiline comment) in FSI? (credit: Tomas)
@@ -219,9 +219,9 @@
Most network-based C libraries refer to socket.h to describe the type of socket that can be used with their API so it’s an important entry point for a lot of network operations and one that would be nice to support as generically as possible in OCaml.
The catch, though, is that, most likely for historical reasons¹, the POSIX specifications only partially defines some of the required data structures and types, which makes it possible to write C code using them but does not give enough information to write C bindings without having to use the compiler to parse the actual system-specific headers of the running host.
For instance, here’s how the sockaddr structure is specified:
-
The <sys/socket.h> header defines the sockaddr structure that includes at least the following members:sa_family_t sa_family address family char sa_data[] socket address (variable-length data)
+
The <sys/socket.h> header defines the sockaddr structure that includes at least the following members:sa_family_t sa_family address family char sa_data[] socket address (variable-length data)
Likewise, here’s what is specified about the size of the socklen_t data type:
-
<sys/socket.h> makes available a type, socklen_t, which is an unsigned opaque integral type of length of at least 32 bits.
+
<sys/socket.h> makes available a type, socklen_t, which is an unsigned opaque integral type of length of at least 32 bits.
Thus, in order to know the exact offset of sa_family inside the sockaddr structure or the actual size of a socklen_t integer, one has to include the OS-specific header, parse its definitions for that specific OS and, only then, is it possible to compute that offset or data size. Let’s see how it’s done in our binding now!
Putting it together
The C binding requires 4 separate passes:
@@ -312,14 +312,14 @@
What we’re after, are more lightweight structures (less than 1kB), that can live fully in a user space, so that we can have even millions of them cooperating frequently with each other without heavy performance penalties.
Before we begin, I think it’s good to discuss different designs. We’ll cover several different topics to be able to make more informed decisions, that we’re up to apply to our own solution.
Preemptive vs cooperative scheduler
Scheduler is a subsystem, which direct responsibility is to assign CPU core processing power to a particular fiber. It’s also responsible for coordinating fibers execution. The two most common categories of schedulers are preemptive and cooperative.
-
A preemptive scheduler is the one, that’s always in control of fiber execution. It’s able to decide on its own, when fiber can be started and stopped. The most obvious example of such is a thread scheduler existing on most operating systems.
+
A preemptive scheduler is the one, that’s always in control of fiber execution. It’s able to decide on its own, when fiber can be started and stopped. The most obvious example of such is a thread scheduler existing on most operating systems.
Preemptive scheduler usually works in one of two ways:
Time based scheduler takes a quant of CPU time and gives it to a given fiber, which ten can execute its logic until it reaches its execution time limit (of course, it can finish earlier). This is how OS thread scheduler, but also how Go goroutine scheduler works.
Another variant is step-based scheduler, which splits fiber’s function body into series of (more or less equal) steps. Then each fiber is given a number of steps to execute before preemption occurs. Example of such is Erlang’s BEAM - it simply allows each process to execute up to 2000 “reductions”, where each reduction is basically a function call. And since in Erlang there are no loops, only tail-recursive functions, this approach works well for long-living iterative processes as well.
-
One of the problems with preemptive schedulers is that they usually need some kind of involvement from the compiler or hosting virtual machine in order to work. For this reason, most of the fiber libraries use cooperative schedulers to perform their work.
-
A cooperative scheduler doesn’t have a concept of preemption - once started by the scheduler, a fiber will execute until it doesn’t give back the control willingly. This is often done with dedicated programming constructs, and often is known as yielding, parking or awaiting.
+
One of the problems with preemptive schedulers is that they usually need some kind of involvement from the compiler or hosting virtual machine in order to work. For this reason, most of the fiber libraries use cooperative schedulers to perform their work.
+
A cooperative scheduler doesn’t have a concept of preemption - once started by the scheduler, a fiber will execute until it doesn’t give back the control willingly. This is often done with dedicated programming constructs, and often is known as yielding, parking or awaiting.
In cooperative variant, a fiber body is usually split into series of discrete steps, between which fiber gives control back to the scheduler.
Keep in mind that these two are not mutually exclusive - a preemptive scheduler often provides a way for a fiber to return control back to it when it’s known that fiber won’t be executing any longer eg. because it has been put to sleep for a while.
Stackless vs. stackful
A concept, that’s somewhat related to a topic above is the idea of stackless and stackful coroutines.
@@ -330,11 +330,11 @@
Finite state machines - this variant is usually faster and can be encoded manually (example of such case is Akka actors), but for a human eye it usually doesn’t really read as a sequential step-by-step program execution, unless it has some support from the compiler itself (see: C# and Rust).
Monadic sequencing via bind/flatMap operator, which is very popular in functional languages. While we cover it in more details in the rest of this blog post, for now it’s enough to say that it’s a way to chain callback-based behaviors together in a way, that resembles standard sequential code.
-
For sure one of the advantages of stackful coroutines is that they’re mono-colored: you can yield/continue coroutine execution from within any other function, while in the stackless variant splits your world into two-colored functions - synchronous and asynchronous - where async one can be only called and yielded safely (without blocking underlying OS thread) from within another async function.
+
For sure one of the advantages of stackful coroutines is that they’re mono-colored: you can yield/continue coroutine execution from within any other function, while in the stackless variant splits your world into two-colored functions - synchronous and asynchronous - where async one can be only called and yielded safely (without blocking underlying OS thread) from within another async function.
Eager vs lazy fibers
We already mentioned two important events in fiber execution life cycle - starting and parking. Here I briefly discuss about different design decisions on when to start a fiber execution.
-
Eager execution means, that fiber is started automatically after its creation. An example of such are Scala Future[A] and JavaScript Promise. Since execution process starts right away, we’re willingly resign from a certain degree of control over how or when to execute given fiber. Usually this is solved by wrapping a fiber creation into another function or lambda.
-
Lazy execution is much more common and preferred way of work, as it allows us to separate place where we want to define our asynchronous sequence of steps from the place, where the execution details are defined. It’s used in C# TPL as well as pretty much in all functional languages implementations (excluding Scala futures mentioned earlier).
-
Interruption
There are also few decisions regarding premature escaping the fiber execution, also known as interruption/cancelation: one of them requires passing special object - a token - between method calls and explicit checking for its completion. It is how C# Tasks work. However putting such requirement onto the API user can be cumbersome and error-prone option. Therefore pretty much every other coroutine library either allows to direcly interrupt a fiber or (like in case of F# Async) passes cancelation tokens and check if they were triggered under the hood.
+
Eager execution means, that fiber is started automatically after its creation. An example of such are Scala Future[A] and JavaScript Promise. Since execution process starts right away, we’re willingly resign from a certain degree of control over how or when to execute given fiber. Usually this is solved by wrapping a fiber creation into another function or lambda.
+
Lazy execution is much more common and preferred way of work, as it allows us to separate place where we want to define our asynchronous sequence of steps from the place, where the execution details are defined. It’s used in C# TPL as well as pretty much in all functional languages implementations (excluding Scala futures mentioned earlier).
+
Interruption
There are also few decisions regarding premature escaping the fiber execution, also known as interruption/cancelation: one of them requires passing special object - a token - between method calls and explicit checking for its completion. It is how C# Tasks work. However putting such requirement onto the API user can be cumbersome and error-prone option. Therefore pretty much every other coroutine library either allows to direcly interrupt a fiber or (like in case of F# Async) passes cancelation tokens and check if they were triggered under the hood.
Implementation
Since we talked a bit about various approaches, let’s get to the meat of this blog post: implementing our own coroutine library in F#. So, what properties will it have?:
We use cooperative scheduling (we don’t want to tweak the compiler) of stackless fibers with support from F# computation expression for nice syntax.
@@ -377,7 +377,7 @@
Now, since our cancellation is not explicit, we need to deal with few things:
Whenever parent fiber is cancelled, all child fibers it spawned are also cancelled.
-
Whenever we cancel a fiber that loose the race, we don’t want to accidentally cancel a token of its parent.
+
Whenever we cancel a fiber that loose the race, we don’t want to accidentally cancel a token of its parent.
This behavior implies at least using two separate tokens, however in practice it will be more pragmatic to make our Cancel token work as a tree hierarchy - this way we can easily keep track of things and support more complex scenarios.
[<Sealed;AllowNullLiteral>] typeCancel(parent: Cancel) = letmutable flag:int=0 letmutable children: Cancel list= [] new() = Cancel(null) /// Check if token was cancelled member __.Cancelled = flag =1 /// Remove child token memberprivate __.RemoveChild(child) = letrec loop child = let children' = children let nval = children' |> List.filter ((<>) child) ifnot (obj.ReferenceEquals(children', Interlocked.CompareExchange(&children, nval, children'))) then loop child ifnot (List.isEmpty children) then loop child /// Create a new child token and return it. member this.AddChild () = letrec loop child = let children' = children if (obj.ReferenceEquals(children', Interlocked.CompareExchange(&children, child::children', children'))) then child else loop child loop (Cancel this) /// Cancel a token member this.Cancel() = if Interlocked.Exchange(&flag, 1) =0then for child in Interlocked.Exchange(&children, []) do child.Cancel() ifnot (isNull parent) then parent.RemoveChild(this)
@@ -488,7 +488,7 @@
letrec run () = match Seq.tryHead timeline with |None-> running <-false |Some (KeyValue(time, bucket)) -> timeline <- Map.remove time timeline currentTime <- time for fn in List.rev bucket do fn () run ()
We’ll try to pick the first entry from the timeline - since here we use F# map, which is sorted in ascending order, we know that first entry is the one with the shortest execution timeout. We update our “current” time to match the expected one we calculated earlier, and finally we execute all functions scheduled at that time and repeat the loop all over until we eventually run out of scheduled actions.
-
Now here’s the trick - we use List.rev to execute functions in the same order in which they were scheduled, because we want our tests to be deterministic and our bugs to be reproducible. However this is not the only strategy - since we know that functions in the same bucket could as well be executing in parallel, we could shuffle them around in different permutations for early discovery of some data races! I’ll won’t dive into it, but leave that idea as food for thoughts for you.
+
Now here’s the trick - we use List.rev to execute functions in the same order in which they were scheduled, because we want our tests to be deterministic and our bugs to be reproducible. However this is not the only strategy - since we know that functions in the same bucket could as well be executing in parallel, we could shuffle them around in different permutations for early discovery of some data races! I’ll won’t dive into it, but leave that idea as food for thoughts for you.
One last note about the test scheduler is that isolating it from the actual physical clock means, we cannot trust our time functions (like DateTime.UtcNow) any longer. This shouldn’t really be an issue though - because relying on physical time would potentially make our tests indeterministic, we didn’t want to use it anyway, right?
However, we need to be able to obtain current time from the scheduler, so we need to extend its API:
typeIScheduler= abstract UtcNow:unit->unit // ... other methods typeTestScheduler() = letmutable currentTime = DateTime.UtcNow.Ticks // ... rest of the implementation interface IScheduler with member __.UtcNow() = DateTime(currentTime) // ... other methods
typeOrder= { items: OrderItem list customer: Customer orderDate: DateTime }
typeOrderItem= { product: Product quantity:int }
typeCustomer= { name:string }
typeProduct= { name:string price:float }
let addItem (order: Order) (item: OrderItem) = { order with items = order.items @ [item] }
let getTotalAmount (order: Order) = let total =0.0 for item in order.items do total <- total + item.price total
let getPrice (item: OrderItem) = item.product.price * item.quantity
let getTotalPrice (order: Order) = let total =0.0 for item in order.items do total <- total + getPrice(item) total
@@ -559,7 +559,7 @@
Maybe aside of the logger we may be needing a separate telemetry mechanism to count number of incoming request or password validation failures? That means another parameter.
Salt generation is pseudo-random process - it we want our function to be deterministic, we should probably parametrize it over explicitly passed Random as well.
-
As you see, what seemed to be simple task at the beginning can quickly blow up out of proportion. As the number of arguments grows, the more nasty our wiring code eventually becomes. Quite common pattern is to hide all of that nastiness under the carpet a.k.a. composition root. However this doesn’t have to be the case.
+
As you see, what seemed to be simple task at the beginning can quickly blow up out of proportion. As the number of arguments grows, the more nasty our wiring code eventually becomes. Quite common pattern is to hide all of that nastiness under the carpet a.k.a. composition root. However this doesn’t have to be the case.
Below we’ll cover another approach for dealing with dependencies - inspired by Scala ZIO library - using incremental steps, from first principles to monadic bindings.
Managing dependencies beyond partial application
Let’s start from how our code from above will eventually look like at the end of this step:
let changePass env =fun req ->task { let! user = Db.fetchUser env req.UserId if user.Hash = bcrypt user.Salt req.OldPass then let salt = Random.bytes env 32 do! Db.updateUser env { user with Salt = salt; Hash = bcrypt salt req.NewPass } Log.info env "Changed password for user %i" user.Id returnOk () else Log.error env "Password change unauthorized: user %i" user.Id returnError"Old password is invalid" }
@@ -587,8 +587,8 @@
It doesn’t impose specific restrictions on libraries and frameworks.
Now we could as well stop here - IMHO this approach is already good and useful for most cases. We can also try to push it further. As you’ve seen, our code now requires quite a lot of env passing around. Could we do something about this? It turns out that yes, we could.
-
Reader monad
Before we continue: what we’re going to cover now is less useful in terms of current state of F# ecosystem for the reasons I’ll mention later.
-
The pattern we’ll use here is known as a Reader Monad. While it’s useful in certain situations, it’s not widely used - IMO it’s fault lies in the name itself, which somehow managed to sound both borderline meaningless and scary in ears of many developers.
+
Reader monad
Before we continue: what we’re going to cover now is less useful in terms of current state of F# ecosystem for the reasons I’ll mention later.
+
The pattern we’ll use here is known as a Reader Monad. While it’s useful in certain situations, it’s not widely used - IMO it’s fault lies in the name itself, which somehow managed to sound both borderline meaningless and scary in ears of many developers.
The rest of this blog post will be introduction to this style in F#, however focused solely around problem of dependency management - we’ll ignore other aspects of monads.
We’ll going to reuse our environment type from above, but now encode it directly into another type we’ll call Effect. Since I’ve mentioned that our pattern has M-word in it, you can safely assume that our handler’s logic will be defined as a lazy sequence of steps to be executed (sounds almost like async/await). In F# we’ll sugar them by using custom computation expression (I’m going to call it effect { ... }) returning our effect type, which we’ll define as:
[<Struct>]typeEffect<'env, 'out>= Effect of ('env->'out)
@@ -921,31 +921,31 @@ await prisma.$transaction(async tx => {
N+1 selects problem 与 Prisma ORM/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/
- N+1 查询问题是指在通过 ORM 查询数据时,执行了一次初始查询来获取父对象列表(这 1 次查询),然后为列表中的每一个父对象都单独执行了一次额外的查询来获取其关联的子对象(这 N 次查询)。最终导致总共执行了 1 + N 次数据库查询,其中 N 是初始查询返回的父对象的数量。
-
举个例子:
+ N+1 查询问题是指在通过 ORM 查询数据时,执行了一次初始查询来获取父对象列表(这 1 次查询),然后为列表中的每一个父对象都单独执行了一次额外的查询来获取其关联的子对象(这 N 次查询)。最终导致总共执行了 1 + N 次数据库查询,其中 N 是初始查询返回的父对象的数量。
+
举个例子:
假设有两个数据库模型:User(用户)和 Post(帖子),一个用户可以有多篇帖子(一对多关系)。
现在,需要获取前 10 个用户以及他们各自的所有帖子。
-
一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:
+
一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:
-
第一次查询 (The “1”): 获取前 10 个用户。
SELECT*FROMUser LIMIT 10;
-
接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。
-- 用户 1 SELECT*FROM Post WHERE authorId =1; -- 用户 2 SELECT*FROM Post WHERE authorId =2; -- 用户 3 SELECT*FROM Post WHERE authorId =3; -- ... 直到 用户 10 SELECT*FROM Post WHERE authorId =10;
+
第一次查询 (The “1”): 获取前 10 个用户。
SELECT*FROMUser LIMIT 10;
+
接下来的 N (=10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。
-- 用户 1 SELECT*FROM Post WHERE authorId =1; -- 用户 2 SELECT*FROM Post WHERE authorId =2; -- 用户 3 SELECT*FROM Post WHERE authorId =3; -- ... 直到 用户 10 SELECT*FROM Post WHERE authorId =10;
-
在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。
+
在这个场景下,总共执行了 1 + 10 = 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。
纯函数 vs. 非纯函数 (Pure vs. Impure Functions): Capture Checking 显式地区分了纯函数和非纯函数。类型为 A => B 的函数被认为是非纯函数,它可以捕获任意 Capability,等价于 A ->{cap} B 1。而类型为 A -> B 的函数则是纯函数,它不能捕获任何 Capability。此外,还可以使用 A ->{c, d} B 的形式来显式指定函数只能捕获 Capability c 和 d。这种区分使得类型系统能够强制执行函数式编程的原则,其中纯函数因其可预测性和可测试性而备受推崇。
纯函数 vs. 非纯函数 (Pure vs. Impure Functions): Capture Checking 显式地区分了纯函数和非纯函数。类型为 A => B 的函数被认为是非纯函数,它可以捕获任意 Capability,等价于 A ->{cap} B 1。而类型为 A -> B 的函数则是纯函数,它不能捕获任何 Capability。此外,还可以使用 A ->{c, d} B 的形式来显式指定函数只能捕获 Capability c 和 d。这种区分使得类型系统能够强制执行函数式编程的原则,其中纯函数因其可预测性和可测试性而备受推崇。
| val later = usingLogFile { f => () => f.write(0) } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |The expression's type () => Unit is not allowed to capture the root capability `cap`. |This usually means that a capability persists longer than its allowed lifetime.
在 Scala 3 中,函数类型 A => B 被认为是不纯的,它可以捕获任意 Capability。实际上,A => B 是 A ->{cap} B 的别名,明确地表明了它可能捕获 “通用 Capability”。这种默认行为反映了 Scala 过去函数可以拥有任意副作用的特点。然而,随着 Capture Checking 的引入,开发者被鼓励更明确地表达函数的纯度。
-
与不纯函数相对的是纯函数,其类型为 A -> B,表示该函数不能捕获任何 Capability。纯函数是函数式编程中的核心概念,Capture Checking 提供了一种在类型层面强制执行纯性的方法。确保函数的纯性可以使代码更具可预测性和可测试性,因为纯函数的输出完全取决于其输入,并且没有副作用。
-
开发者还可以指定函数可以捕获的特定 Capability,语法为 A ->{c, d} B,表示该函数可以捕获 Capability c 和 d。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。
-
捕获注解^ 的优先级高于 -> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,A ^ C -> B 表示一个从捕获的 A 到 B 的纯函数。
+
在 Scala 3 中,函数类型 A => B 被认为是不纯的,它可以捕获任意 Capability。实际上,A => B 是 A ->{cap} B 的别名,明确地表明了它可能捕获 “通用 Capability”。这种默认行为反映了 Scala 过去函数可以拥有任意副作用的特点。然而,随着 Capture Checking 的引入,开发者被鼓励更明确地表达函数的纯度。
+
与不纯函数相对的是纯函数,其类型为 A -> B,表示该函数不能捕获任何 Capability。纯函数是函数式编程中的核心概念,Capture Checking 提供了一种在类型层面强制执行纯性的方法。确保函数的纯性可以使代码更具可预测性和可测试性,因为纯函数的输出完全取决于其输入,并且没有副作用。
+
开发者还可以指定函数可以捕获的特定 Capability,语法为 A ->{c, d} B,表示该函数可以捕获 Capability c 和 d。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。
+
捕获注解 ^ 的优先级高于 -> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,A ^ C -> B 表示一个从捕获的 A 到 B 的纯函数。
与函数类型类似,Capture Checking的概念也延伸到了命名参数类型。=> Int 允许任意Capability引用,类似于不纯函数类型。-> Int 禁止任何Capability引用,类似于纯函数类型。而 ->{c} Int 则只允许引用Capability c。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。
与函数类型类似,Capture Checking的概念也延伸到了命名参数类型。=> Int 允许任意Capability引用,类似于不纯函数类型。-> Int 禁止任何Capability引用,类似于纯函数类型。而 ->{c} Int 则只允许引用Capability c。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。
let changePass req =effect { let! user = Db.fetchUser req.UserId let! salt = Random.bytes 32 do! Log.info "Password updated for user %i" user.Id returnOk() }
memberpublic this.UpdateMyName (command: UpdateUsernameCommand) (user: User) = let user = userRepository.GetById user.Id let oldName = user.Username let newName = command.Username