在阅读/观看了各种介绍和博客文章以及到处有的一些代码之后,做了一些Try F#教程并开始阅读“ Real World Functional Programming”,我觉得我应该能够编写第一个小型的实际应用程序在F#中。

为了能够专注于掌握语言,我决定不尝试解决新问题,而是将现有的C#应用​​程序移植到我希望可以忍受的内容上惯用的F#。

“应用程序”是Microsoft Dynamics CRM的所谓“插件”。 (一个插件是IPlugin接口的实现,然后被注册为可以在某些平台操作上调用,与数据库触发器相比,可能是最好的选择。)
对于C#插件,我有一个用于一般基础结构的基本库,而这个特定的插件本身(根据NCrunch)是613行C#代码。对于F#实现,我决定不使用其他库,而是重新实现了我在F#中所需的功能。结果是该插件的F#代码为68行,到目前为止,新的小型F#库为97行。

总体而言,移植C#代码并不难,因为我的SOLID类几乎映射了直接功能。对于更多的“传统”面向对象代码,这可能会大不相同。 ;但这仍然不是我主要关心的问题。

我主要对这些东西感兴趣:


F#代码的惯用法如何?我喜欢模式匹配,并有机会使用它。在所有情况下都是一个好主意吗?函数的命名(其中有些肯定是奇怪的,因为我保留了C#版本的类名)和值怎么办?
对于最终不带任何参数的组合函数(那些只是带有构造函数注入依赖项的类,以及在C#中为只读公共属性的类),当场获得结果并将其传递就更自然了而不是传递函数?
在我的SOLID C#代码中,我通过构造函数注入构建了广泛的对象图,并使用应用程序的合成根目录中的IoC容器进行了连接。我是功能性的F#代码,既没有接口也没有构造函数,所以我知道构造函数注入实质上已由函数组合代替,但是我仍然不清楚如何在现实世界的应用程序中以类似的可维护和可操作的方式来实现该功能。可以像我在C#中具有合成根的方式进行演化。我已经在此处的Stack Overflow上对此问题进行了详细阐述,因此在此不再赘述,但是我在Composition模块中采用了我在此处所描述的“注册表”。这实质上是使用“穷人的DI”(如Mark Seemann所称)的构图根。这是一个好主意吗?还有什么替代方法?

这是库:

namespace Dymanix

module Mscrm =
    open System
    open Microsoft.Xrm.Sdk
    open Microsoft.Xrm.Sdk.Messages

    type CrmRecord =
        | Entity of Entity
        | Reference of EntityReference

    type PluginMessage =
        | Create
        | Update
        | Delete
        | Other of string

    type PreStage =
        | PreValidation
        | PreOperation

    type Stage =
        | Pre of PreStage
        | PostOperation

    type StepStage =
        { Message : PluginMessage
          Stage : Stage }

    type FullRecordImage = Entity

    type TargetRecord = Entity

    let MergeRecords (baseRecord : Entity) (toMerge : Entity) =
        let mergeTarget = Microsoft.Xrm.Sdk.Entity(baseRecord.LogicalName, Id = baseRecord.Id)
        toMerge.Attributes |> Seq.iter (fun att -> mergeTarget.[att.Key] <- att.Value)
        mergeTarget

    let GetMessage(context : IPluginExecutionContext) =
        match context.MessageName with
        | "Create" -> Create
        | "Update" -> Update
        | "Delete" -> Delete
        | message -> Other(message)

    let GetStage(context : IPluginExecutionContext) =
        match context.Stage with
        | 10 -> Pre(PreValidation)
        | 20 -> Pre(PreOperation)
        | 40 -> PostOperation
        | _ -> failwith "Invalid plugin stage"

    let GetStepStage(context : IPluginExecutionContext) =
        { Message = GetMessage context
          Stage = GetStage context }

    let GetTarget(context : IPluginExecutionContext) = context.InputParameters.["Target"] :?> TargetRecord

    let GetFullRecordImage(context : IPluginExecutionContext) =
        match GetStepStage context with
        | { Message = Delete; Stage = _ } -> context.PreEntityImages.["Image"]
        | { Message = Create; Stage = Pre(_) } -> GetTarget context
        | { Message = Update; Stage = Pre(_) } -> MergeRecords context.PreEntityImages.["Image"] (GetTarget context)
        | { Message = _; Stage = Pre(_) } -> context.PreEntityImages.["Image"]
        | { Message = _; Stage = PostOperation } -> context.PostEntityImages.["Image"]

    let DecomposeServiceProvider(serviceProvider : IServiceProvider) =
        let organizationServiceFactory =
            serviceProvider.GetService typeof<IOrganizationServiceFactory> :?> IOrganizationServiceFactory
        let context = serviceProvider.GetService typeof<IPluginExecutionContext> :?> IPluginExecutionContext
        let organizationService = organizationServiceFactory.CreateOrganizationService(System.Nullable(context.UserId))
        let tracingService = serviceProvider.GetService typeof<ITracingService> :?> ITracingService
        (context, organizationService, tracingService, organizationServiceFactory)

    module Utility =
        let GetPrimaryNameField (organizationService : IOrganizationService) entityName =
            let request = RetrieveEntityRequest(LogicalName = entityName)
            let response = organizationService.Execute(request) :?> RetrieveEntityResponse
            response.EntityMetadata.PrimaryNameAttribute

        let private getRecordName getPrimaryNameField (organizationService : IOrganizationService) record =
            let reference =
                match record with
                | Entity(e) -> e.ToEntityReference()
                | Reference(r) -> r

            let primaryNameField = getPrimaryNameField organizationService reference.LogicalName

            let entity =
                match record with
                | Entity(e) -> e
                | Reference(r) ->
                    organizationService.Retrieve(r.LogicalName, r.Id, Query.ColumnSet([| primaryNameField |]))
            string (entity.GetAttributeValue<string>(primaryNameField))

        let GetRecordName(organizationService : IOrganizationService) =
            getRecordName GetPrimaryNameField organizationService


这是插件代码-实际的“应用程序”:

namespace SetClassificationIdentificationFields

open System
open Microsoft.Xrm.Sdk
open Microsoft.Xrm.Sdk.Messages
open System.Xml.Linq
open Dymanix.Mscrm
open Dymanix.Mscrm.Utility

module Domain =
    let (!<>) name = XName.Get(name)

    let NameAddedRegardingObject getRecordName (innerRegardingObject : unit -> EntityReference option) =
        let reference = innerRegardingObject()
        match reference with
        | Some(record) ->
            if Guid.Empty <> record.Id then record.Name <- getRecordName ((Reference(record)))
        | _ -> ()
        reference

    let ListFindingRegardingObject regardingLookups (image : FullRecordImage) =
        let existingLookups = regardingLookups() |> List.filter image.Contains
        match existingLookups with
        | [] -> None
        | [ a ] -> Some(image.GetAttributeValue<EntityReference>(a))
        | _ -> failwith "Only one parent lookup may contain data"

    let ConfiguredRegardingLookups(configuration : XElement) =
        configuration.Element(!<>"regardinglookups").Elements(!<>"lookup")
        |> List.ofSeq
        |> List.map (fun e -> e.Attribute(!<>"name").Value)

    let AllRequiredAttributesArePresent(target : TargetRecord) =
        [ "gcnm_recordid"; "gcnm_recordtype"; "gcnm_recordname" ] |> List.forall target.Contains

    let SetClassificationIdentificationValues (target : TargetRecord) (reference : EntityReference option) =
        match reference with
        | Some(record) ->
            target.["gcnm_recordname"] <- record.Name
            target.["gcnm_recordtype"] <- record.LogicalName
            target.["gcnm_recordid"] <- record.Id.ToString()
        | None -> ()

    let Main requiredAttributesArePresent getTarget regardingObject setValues =
        if (not (requiredAttributesArePresent getTarget)) then setValues (regardingObject)

module Composition =
    open Domain

    let Compose serviceProvider configuration =
        let (context, organizationService, _, _) = DecomposeServiceProvider serviceProvider
        let configurationXml = XElement.Parse configuration
        let getRecordName = GetRecordName organizationService
        let regardingLookups() = ConfiguredRegardingLookups configurationXml
        let listFindingRegardingObject() = ListFindingRegardingObject regardingLookups (GetFullRecordImage(context))
        let regardingObject = NameAddedRegardingObject getRecordName listFindingRegardingObject
        let setValues = SetClassificationIdentificationValues(GetTarget context)
        let allAttributesPresent = AllRequiredAttributesArePresent
        let main() = Main allAttributesPresent (GetTarget context) regardingObject setValues
        main

module Plugin =
    type Plugin(configuration) =
        member this.Configuration = configuration
        interface IPlugin with
            member this.Execute serviceProvider =
                let main = Composition.Compose serviceProvider this.Configuration
                main()


#1 楼

总的来说,我认为还不错。

基于快速扫描,有一些建议可以跳出来。

注意:这段代码是在浏览器中编写的,未经编译-抱歉,有任何错误,但您应该明白。

1)下面SetClassificationIdentificationValues中的操作似乎很常见。

我将其封装为:

let SetEntityReference (target:TargetRecord) (reference : EntityReference) =
  target.["gcnm_recordname"] <- record.Name
  target.["gcnm_recordtype"] <- record.LogicalName
  target.["gcnm_recordid"] <- record.Id.ToString()


,那么您可以简化将其用于以下代码:
可能会找到其他可以使用它的地方。

2)使用与选项匹配通常可以用Option.map或Option.bind代替。

例如:

let SetClassificationIdentificationValues target reference  =
    reference 
    |> Option.iter (SetEntityReference target)


可以更改为:

let NameAddedRegardingObject getRecordName (innerRegardingObject : unit -> EntityReference option) =
    let reference = innerRegardingObject()
    match reference with
    | Some(record) ->
        if Guid.Empty <> record.Id then record.Name <- getRecordName ((Reference(record)))
    | _ -> ()
    reference


3)下面的代码具有将可能的多项目列表到一个项目列表(又称选项!)

/// return Some if the record has a non-empty Guid, else None
let filterIfValidGuid record =
    if Guid.Empty <> record.Id then Some record else None

let NameAddedRegardingObject getRecordName (innerRegardingObject : unit -> EntityReference option) =
    let updateName record =
       record.Name <- Reference(record) |> getRecordName 

    innerRegardingObject()
    |> Option.bind filterIfValidGuid  // filter
    |> Option.iter updateName // update only if filtered ok  


您可以将其写为完全通用的实用程序函数:

 let ListFindingRegardingObject regardingLookups (image : FullRecordImage) =
    let existingLookups = regardingLookups() |> List.filter image.Contains
    match existingLookups with
    | [] -> None
    | [ a ] -> Some(image.GetAttributeValue<EntityReference>(a))
    | _ -> failwith "Only one parent lookup may contain data"

,然后主代码变为

 let listToOption list =
    match list with
    | [] -> None
    | [ a ] -> Some(a)
    | _ -> failwith "Expected only one item"


但是为什么SetEntityReference甚至根本不返回列表?它可以返回两个项目吗?

我不喜欢有异常,除了永远不应该发生的事情之外,因此,如果有两个项目的情况是可能的,一种方法是更改​​它以返回必须由错误处理的错误。客户端。

 let ListFindingRegardingObject regardingLookups (image : FullRecordImage) =
    let existingLookups = regardingLookups() |> List.filter image.Contains
    existingLookups 
    |> listToOption 
    |> Option.map (fun a -> image.GetAttributeValue<EntityReference>(a))


如果可能的话,最好还是将所有这些内容从客户端隐藏起来。我建议使用一个更简单的函数,例如regardingLookups(),该函数在给定谓词的情况下只能返回一个选项。然后,代码简化为:

 let listToOption list =
    match list with
    | [] -> Choice1Of2 None // success
    | [ a ] -> Choice1Of2 Some(a) // success 
    | _ -> Choice2Of2 "Expected only one item"


我假设regardingLookup中的代码与之相关?

 let ListFindingRegardingObject regardingLookup (image : FullRecordImage) =
    regardingLookup image.Contains
    |> Option.map (fun a -> image.GetAttributeValue<EntityReference>(a))


如果您打算将大量内容过滤到Option中,这是我的版本:

let ConfiguredRegardingLookups(configuration : XElement) =
    configuration.Element(!<>"regardinglookups").Elements(!<>"lookup")
    |> List.ofSeq
    |> List.map (fun e -> e.Attribute(!<>"name").Value)


此版本保证可以返回一个选项,并且不会浪费任何将seq转换为列表的工作,也不需要ConfiguredRegardingLookups之类的东西。

要总结此注释,请将列表或序列转换为一个选项可能是代码气味。但是,如果必须这样做,请编写一个泛型函数。另请参阅https://stackoverflow.com/questions/7487541/optionally-take-the-first-item-in-a-sequence

太频繁使用匹配(在选项或列表中)通常是一种表示您可以编写更多通用代码。

您可以看到,一般而言,我正在尝试尽可能多地使用listToOption和其他内置函数,然后创建实用程序

希望有帮助!

评论


\ $ \ begingroup \ $
谢谢斯科特!那是很多有趣的信息。我特别不了解Option函数,该函数确实使代码比匹配更简化,并且我也完全没有听说Choice。
\ $ \ endgroup \ $
–TeaDrivenDev
2014年3月13日在1:38

\ $ \ begingroup \ $
“你怎么敢批评主人!”我心中有声音说。我喜欢在代码中撒上非常易读的声明,例如“ Person person”和“ Order order”,并在其上加上阴影,但是我尽量避免使用“ list”和“ option”,因为它们是关键字。但是现在我在想:“为什么不呢?我会跟随主人的!如果他能做到,我也可以。”与C#不同,F#不会介意。 (这与上面的“ listToOption列表”有关。)
\ $ \ endgroup \ $
–本特·特兰伯格
18年7月7日在4:28



#2 楼

我实际上对代码没有任何实质性的评论,只是有关语法的一些小注释。

这只是一种意见,但我认为自定义!<>运算符有点时髦。一种替代方法是使用类型扩展将方法添加到XElementXAttribute类型:

type XContainer with
    member this.Element(name) = this.Element(XName.Get name)
    member this.Elements(name) = this.Elements(XName.Get name)

type XElement with
    member this.Attribute(name) = this.Attribute(XName.Get name)


类型扩展(也称为扩充)允许您扩展现有类型。类似于C#中的扩展方法。一旦在模块中定义了这些方法,就可以使用字符串调用这些方法。

这只是一个首选项。

关于命名约定,我注意到《 F#组件设计指南》中提到,让绑定值(包括函数)通常是小写的,尤其是内部的。

根据MSDN,记录模式不必包含不匹配的字段,因此显然不需要Message = _形式的通配符。但是,该文章中给出的示例很糟糕,因为它使用了通配符。

这段代码中有很多地方可以省略一些括号,但这是有根据的。 Pre(PreValidation)可能变成Pre Prevalidationif (not (requiredAttributesArePresent getTarget))可能变成if not (requiredAttributesArePresent getTarget)getRecordName ((Reference(record)))可能变成getRecordName (Reference record),依此类推。通常,在F#特定元素上加上括号似乎很惯用,而在成员声明/调用上删除括号似乎不太常见。我认为实际上很难在F#中正确地进行括号化,因为括号并非总是必需的,并且记住异常比规则更难。

评论


\ $ \ begingroup \ $
感谢卢克!是的,操作员有点花哨;我只是想尝试一下,因为F#允许我这样做。 ;-)类型扩展对于实际使用似乎没有什么麻烦;我认为那会属于图书馆。我选择了UpperCamelCased函数名称,以使“基元”和组成部分分开。还有其他好的方法吗?至于括号,自从我编写代码以来,我已经改变了习惯。仅来自C#,而没有它们,F#代码的读取效果会更好。
\ $ \ endgroup \ $
–TeaDrivenDev
2014年3月16日下午3:18

\ $ \ begingroup \ $
如果您计划拥有许多这些插件和一个通用代码库来支持它们,那么将类型扩展放入库中可能是合适的。或者,如果不是,那也很好-类型扩展对于这种即席即弃的功能真的非常有用。
\ $ \ endgroup \ $
–luksan
2014年3月16日下午4:03

\ $ \ begingroup \ $
我不知道命名问题。要记住的一件事是,let绑定,参数等将遮盖任何先前的阴影(至少在当前作用域中),如果希望避免意外引用原始阴影,则在某些情况下实际上可能是需要的。
\ $ \ endgroup \ $
–luksan
2014年3月16日下午4:03

\ $ \ begingroup \ $
利用阴影是有趣的一点。我知道一些可能有用的地方。我不知道实际上以这种方式工作是多么习惯。
\ $ \ endgroup \ $
–TeaDrivenDev
2014年3月17日下午0:42

\ $ \ begingroup \ $
luksan所指的技术对我来说很重要,不仅要避免意外地引用原始文件,而且还因为它使代码更具可读性。您获得的标识符更少,通常更少。您可以轻松地跟踪数据的可能路径,而不必记住所有可能采用的不同名称。这没有任何问题。至于惯用语-我会投票赞成。
\ $ \ endgroup \ $
–本特·特兰伯格
18年7月7日在4:04