Fyo*_*kin 7 f# functional-programming
免责声明:
虽然我接受了不可变状态和高阶函数的福音,但我的实际经验仍然是95%面向对象.我想改变这一点,但是更多的是什么.所以我的大脑非常接近OO.
问题:
我经常遇到这种情况:一个业务功能实现为一个小的"核心"加上多个"插件",共同为用户呈现一个看似坚实的表面.我发现这种"微内核"架构在很多情况下都能很好地工作.另外,非常方便,它与DI容器很好地结合,可以用于插件发现.
那么,我该如何以功能的方式做到这一点?
我不认为这种技术的基本思想本质上是面向对象的,因为我刚刚在不使用任何OO术语或概念的情况下对其进行了描述.但是,我不能完全围绕功能性的方式来解决它.当然,我可以将插件表示为函数(或函数桶),但是当插件需要将自己的数据作为整体图片的一部分时,困难的部分就出现了,而插件到插件的数据形状也不同.
下面是一个小的F#片段,它或多或少是C#代码的字面翻译,我将从头开始实现这个模式时编写.
注意弱点:丢失类型信息CreateData,必要的向上翻译PersistData.
我每次都对演员阵容(无论是向上还是向下)都畏缩,但我已经学会了接受它们作为C#中必不可少的邪恶.然而,我过去的经验表明,功能方法通常会为这类问题提供意想不到的,美丽而优雅的解决方案.这是我追求的解决方案.
type IDataFragment = interface end
type PersistedData = string // Some format used to store data in persistent storage
type PluginID = string // Some form of identity for plugins that would survive app restart/rebuild/upgrade
type IPlugin = interface
abstract member UniqueID: PluginID
abstract member CreateData: unit -> IDataFragment
// NOTE: Persistence is conflated with primary function for simplicity.
// Regularly, persistence would be handled by a separate component.
abstract member PersistData: IDataFragment -> PersistedData option
abstract member LoadData: PersistedData -> IDataFragment
end
type DataFragment = { Provider: IPlugin; Fragment: IDataFragment }
type WholeData = DataFragment list
// persist: WholeData -> PersistedData
let persist wholeData =
let persistFragmt { Provider = provider; Fragment = fmt } =
Option.map (sprintf "%s: %s" provider.UniqueID) (provider.PersistData fmt)
let fragments = wholeData |> Seq.map persistFragmt |> Seq.filter Option.isSome |> Seq.map Option.get
String.concat "\n" fragments // Not a real serialization format, simplified for example
// load: PersistedData -> WholeData
let load persistedData = // Discover plugins and parse the above format, omitted
// Reference implementation of a plugin
module OnePlugin =
type private MyData( d: string ) =
interface IDataFragment
member x.ActualData = d
let create() =
{new IPlugin with
member x.UniqueID = "one plugin"
member x.CreateData() = MyData( "whatever" ) :> _
member x.LoadData d = MyData( d ) :> _
member x.PersistData d =
match d with
| :? MyData as typedD -> Some typedD.ActualData
| _ -> None
}
Run Code Online (Sandbox Code Playgroud)
IPlugin用来绑定在一起UniqueID和CreateData; 如果不是界面,我会使用类似形状的记录.并且IDataFragment用于限制数据片段的类型,否则我将不得不使用obj它们,这将给我更少的类型安全性.(我甚至无法想象如何在Haskell中使用Dynamic,而不是使用Dynamic)我只能同情你的陈述.虽然小编程中的函数式编程已经被人们讨论过,但对于如何在大型函数式编程中进行函数编程却没有什么建议.我认为对于F#而言,随着系统的发展,大多数解决方案将倾向于更加面向对象(或者至少是面向接口的).我认为这不一定是坏事 - 但如果有一个令人信服的FP解决方案,我也希望看到它.
我在类似场景中使用过的一种模式是拥有一对接口,一个是打字的,一个是非打字的,还有一个基于反射的机制.所以在你的场景中你会有这样的事情:
type IPlugin =
abstract member UniqueID: PluginID
abstract member DataType: System.Type
abstract member CreateData: unit -> IDataFragment
type IPlugin<'data> =
inherit IPlugin
abstract member CreateData: unit -> 'data
abstract member PersistData: 'data -> PersistedData option
abstract member LoadData: PersistedData -> 'data
Run Code Online (Sandbox Code Playgroud)
一个实现看起来像这样:
let create() =
let createData () = MyData( "whatever" )
{
new IPlugin with
member x.UniqueID = "one plugin"
member x.DataType = typeof<MyData>
member x.CreateData() = upcast createData()
interface IPlugin<MyData> with
member x.LoadData d = MyData( d )
member x.PersistData (d:MyData) = Some d.ActualData
member x.CreateData() = createData()
}
Run Code Online (Sandbox Code Playgroud)
请注意,这CreateData是两个接口的一部分 - 它只是用来说明在打字和无类型接口之间复制多少以及需要跳过箍以在它们之间进行转换的频率之间存在平衡.理想情况下CreateData不应该在那里IPlugin,但如果它节省你的时间,我不会回头两次.
从开始IPlugin到IPlugin<'a>你需要一个基于反射的辅助函数,但至少你明确地知道了类型参数,因为它是IPlugin接口的一部分.虽然它并不漂亮,但至少类型转换代码包含在代码的单个部分中,而不是分散在所有插件中.
您不必定义接口即可使架构可插入 F# 中。函数已经是可组合的。
您可以从外到内编写系统,从系统所需的整体行为开始。例如,这是我最近编写的一个函数,它将轮询消费者从未收到消息的状态转换为新状态:
let idle shouldSleep sleep (nm : NoMessageData) : PollingConsumer =
if shouldSleep nm
then sleep () |> Untimed.withResult nm.Result |> ReadyState
else StoppedState ()
Run Code Online (Sandbox Code Playgroud)
这是一个高阶函数。当我编写它时,我发现它依赖于辅助函数shouldSleep和sleep,所以我将它们添加到参数列表中。然后编译器会自动推断egshouldSleep必须具有类型NoMessageData -> bool。该函数是一个Dependency。函数也是如此sleep。
第二步,shouldSleep函数的合理实现最终看起来像这样:
let shouldSleep idleTime stopBefore (nm : NoMessageData) =
nm.Stopped + idleTime < stopBefore
Run Code Online (Sandbox Code Playgroud)
如果您不知道它的作用,也没关系。这里重要的是函数的组合。在本例中,我们了解到这个特定shouldSleep函数的类型为TimeSpan -> DateTimeOffset -> NoMessageData -> bool,它与不完全相同NoMessageData -> bool。
不过,它非常接近,您可以使用部分函数应用程序来完成其余的距离:
let now' = DateTimeOffset.Now
let stopBefore' = now' + TimeSpan.FromSeconds 20.
let idleTime' = TimeSpan.FromSeconds 5.
let shouldSleep' = shouldSleep idleTime' stopBefore'
Run Code Online (Sandbox Code Playgroud)
该shouldSleep'函数是该函数的部分应用shouldSleep,并且具有所需的类型NoMessageData -> bool。您可以将此函数idle与其依赖项的实现一起组合到该函数中sleep。
由于低阶函数具有正确的类型(正确的函数签名),因此它就位了;无需铸造即可实现此目的。
、idle和函数可以在不同的模块、不同的库中定义,并且您可以使用类似于Pure DI 的shouldSleep过程将它们全部组合在一起。shouldSleep'
如果您想查看从各个函数组成整个应用程序的更全面的示例,我在我的F# Pluralsight 功能架构课程中提供了一个示例。