首先,抱歉我的英语很差,我会尽力让自己清楚,但如果有什么不合理的地方,请指出,我会尝试重新表述。这个问题会很大,这是我第一次寻求有关编程的帮助(到目前为止,我总是在 google 上找到解决方案或想出一些可以解决问题的方法),在这种情况下,我既不了解该语言,也不了解是时候完成学习过程了,真的需要一些经验丰富的建议(最初发布在 Reddit 上:https ://www.reddit.com/r/fsharp/comments/con2aj/new_to_f_help_with_unobtrusive_logging/来 SO 寻求更大的支持)。
TL;博士; 我与单子组合的中缀运算符斗争,利用面向铁路的编程,但无法检索方法信息以进行 AOP 日志记录。我真的很想知道是否有一种方法可以将自动引号与中缀运算符结合起来,或者是否有另一种形式来获取有关中缀类绑定操作或普通 let-binding 前缀中的函数的元信息(不需要静态成员)。我并不特别担心性能,因为这不是一直在运行的东西,它更像是用于特定情况的调试级别日志记录。
一些背景:
我对 F# 完全陌生,我在各种语言(包括一点 C#)方面有 7 年的经验,并且从一开始就喜欢函数式编程,但从来没有机会在我的工作中使用它。现在我有机会将 F# 用于数据转换管道,我真的很想给人留下好印象。
所以我已经用 F# 玩了大约三周了,我完成了我的小应用程序的一些构建块,并且对横切关注点(尤其是日志记录)有些头疼。我真的很喜欢 AOP(面向方面编程)的日志记录方法(不是弄乱逻辑而是装饰函数),并且想在 F# 中使用类似的东西。我做了我的研究,似乎计算表达式是要走的路(或者至少在 AOP F# 搜索中总是会出现这种情况)......但我真的很喜欢在管道中链接操作,这很有意义我正在建模的领域,因为它的主要重点是数据结构遍历和应用于源的转换管道。
我爱上了 F# 的简洁性(我是管道 (|>) 运算符的忠实粉丝)和强大的功能,并且正在寻找一些方便的语法,使一堆链接操作在记录某些内容的必要性中看起来简单而明确沿链采取的行动....这就是我到目前为止遇到的情况,确实需要一些指导:
- 报价单
- 类型扩展
- 引用第二个想法
- 结论和帮助
我真的是新手,所以如果您认为我遗漏了一些愚蠢且完全明显的东西,请不要犹豫,让我知道,我所做的可能完全错误 xD。废话不多说,这是我要装饰的起始代码
type DSError =
....
type DataStructure =
...
module DataStructure =
let root : 'node =
...
let firstChild (node:'node) (data: DataStructure) : Result<'node,DSError> =
...
let nextSibling (node:'node) (data: DataStructure) =
...
module Operators =
let inline ( @> ) data node = (Ok node, data)
let inline ( >>= ) (result, data) func =
match result with
| Error err -> Error err, data
| Ok res -> func res data, data
module TraverseExamples =
open Operators
let arbitraryTraverse (data: DataStructure) =
data @> root
>>= DataStructure.firstChild
>>= DataStructure.nextSibling
>>= DataStructure.firstChild
我正在寻找这样的东西,>>=!
日志装饰形式在哪里>>=
:
data @> root
>>=! DataStructure.firstChild // This function will log
>>= DataStructure.nextSibling // This won't
// executed: DataStructure.firstChild
// arguments: Ok root, [/some inmutable data structure/]
// result: Ok firstChildOfRoot, [/some inmutable data structure/]
所以我唯一不知道的是如何获取作为参数传递的函数的名称和模块。
1. 语录
我发现的第一件事是关于引文。
从第一个(接受的答案下的一些回复),我知道不仅引用是提取此类信息的方法,而且可以使用[<ReflectedDefinition>]
属性从普通函数自动转换为参数。从第二个中,我得到了一种获取正确 FSharp 名称的形式。这是我尝试过的:
let inline ( >>=! ) (result, data) [<ReflectedDefinition>]func =
//Unexpected symbol '[<' in binding
但是[<ReflectedDefinition>]
,属性 sin general 似乎只适用于静态成员。所以我尝试了使用有区别的联合来识别运算符的静态成员方法:
type Logger<'T> =
| LogResult of ('T)
static member bind(LogResult tuple, [<ReflectedDefinition>]func:Expr<_->_>) =
match func with
| DerivedPatterns.Lambdas(_, Patterns.Call(_,methodInfo,_)) ->
printfn "%s" methodInfo.Name
| Patterns.ValueWithName(_, _, data) -> printfn "%s" data
| _ -> printfn "%s" "invalid"
static member ( >>=! ) (LogResult tuple, [<ReflectedDefinition>]func:Expr<_->_>) =
match func with
| DerivedPatterns.Lambdas(_, Patterns.Call(_,methodInfo,_)) ->
printfn "%s" methodInfo.Name
| Patterns.ValueWithName(_, _, data) -> printfn "%s" data
| _ -> printfn "%s" "invalid"
// Prefix form worked as expected. Both print firstChild
Logger.bind(LogResult (data, root), DataStructure.firstChild)
(>>=!)(LogResult (data, root), DataStructure.firstChild)
// This doesnt work:
// type constrain mismatch 'a -> 'b -> Result<'a,'c>
// is not compatible with Expr<'a -> 'b>
LogResult (data, root) >>=! DataStructure.firstChild
我认为这两个参数都不是来自 Logger 类型是我的错(它预期中缀静态操作适用于相同类型)所以我稍微改变了方法只是为了测试我的假设。假设我有一个用于引用的 eval 函数(例如来自 LINQ 或 Unquote 的函数),quotationEval
我可以执行以下操作:
type Logger<'T,'U> =
| LogResult of ('T * 'U)
| LogFunc of ('T -> 'U -> 'T)
...
...
static member ( >>=! ) (LogResult tuple, [<ReflectedDefinition>]logFunc) =
let logFuncUnquoted = quotationEval(logFunc)
match logFuncUnquoted with
| LogFunc func ->
match <@ func @> with
| DerivedPatterns.Lambdas(_, Patterns.Call(_,methodInfo,_)) ->
printfn "%s" methodInfo.Name
| Patterns.ValueWithName(_, _, data) -> printfn "%s" data
| _ -> printfn "%s" "invalid"
| _ -> printfn "%s" "more invalid"
// This doesnt work either:
// type constrain mismatch Logger<'a,'b>
// is not compatible with Expr<Logger<'a,'b>>
LogResult (data, root) >>=! LogResult DataStructure.firstChild
似乎第一个示例没问题,而问题在于中缀运算符。我真的很想使用中缀表示法而不是前缀命名函数来进行组合,但是如果我在 let bounded>>=!
中使用这些函数,从 Logger 更改其中一个函数会怎样:
type Logger<'T> =
...
static member logFunc([<ReflectedDefinition>]func:Expr<_->_>) =
match func with
| DerivedPatterns.Lambdas(_, Patterns.Call(_,methodInfo,_)) ->
printfn "The function called was: %s" methodInfo.Name
| Patterns.ValueWithName(_, _, data) -> printfn "%s" data
| _ -> printfn "%s" "invalid"
let inline ( >>=! ) (result, data) func =
match result with
| Error err -> Error err, data
| Ok res ->
Logger.logFunc func
func res data, data
data @> root
>>=! DataStructure.firstChild
// The function called was: func
好的,不,这也不起作用。在上面的示例中,如果它起作用,我match <@ func @> with
会遇到同样的问题。我需要将函数直接传递给 with ,[<ReflectedDefinition>]
因为如果我不这样做,函数的名称将取自参数。再次使用前面描述的 eval 函数,quotationEval
我可以执行以下操作:
let inline ( >>=! ) (result, data) func =
match result with
| Error err -> Error err, data
| Ok res ->
Logger.logFunc func
let f = quotationEval func
f res data, data
data @> root
>>=! <@ DataStructure.firstChild @>
但我认为<@ ... @>
语法是相当随意的,与日志记录无关,除了作为这种情况的一种方便的黑客攻击,所以我认为也许......我可以扩展函数(扩展是一个静态成员,所以它原则上可以接受[<ReflectedDefinition>]
属性) 并让这个 hack 在 API 中不可见。
2.类型扩展
我专门寻找扩展函数类型,所以这就是我发现的:
- https://docs.microsoft.com/es-es/dotnet/fsharp/language-reference/type-extensions
- 具有 F# 函数类型的扩展方法
- 类型扩展中的重载运算符
在这种情况下,因为我需要这两个函数,所以第一个元组是一个常量函数,给定一个单元返回元组。此外,我只使用第一个函数对元组的链接执行进行编码,我的目标只是知道中缀运算符是否在类型扩展中工作:
[<Extension>]
type FunctionExtension() =
[<Extension>]
static member inline ( >>=! ) (f: unit -> 'a*'b) (g: 'a -> 'b -> 'c) =
(fun (x, y) -> g x y) (f())
[<Extension>]
static member inline bind(f: unit -> 'a*'b, g: 'a -> 'b -> 'c) =
(fun (x, y) -> g x y) (f())
let init = (fun () -> (root, data))
// Prefix form worked as expected. Both execute
init.bind DataStructure.firstChild
(>>=!)(init, DataStructure.firstChild)
// This doesnt work:
// expecting a type supporting the operation >>=!
init >>=! DataStructure.firstChild
在这种情况下,它甚至不起作用(而且我还没有将 g 定义为 Expr< -> >),中缀运算符从未被识别。在前面的例子中,问题出在类型上,而不是识别上。通读第三个链接(类型扩展中的重载运算符)中的答案,看来我需要一些努力才能让中缀运算符在类型扩展中工作,但鉴于它在该解释中有一个中间 let bounded 函数,整点抽象元数据永远不会像我之前的例子那样奏效。
那很快......我当时用不同的方法回到报价单
3.引文第二个想法
自从我开始调查并尝试不同的事情以来,已经过去了将近 2 个工作日,当我开始以不同的方式思考时,我几乎要放弃了。它使用了与第一部分几乎相同的东西,但不是在链接函数中,而是作为实际的“装饰器”,也许我们可以得到这样的东西:
data @> root
>>= log DataStructure.firstChild
>>= DataStructure.nextSibling
>>= log DataStructure.firstChild
日志使获取函数元数据的工作繁重,并且仅在我认为需要时才适用。它看起来简洁明了,我能用我所学的东西实现这样的目标吗?我想我可以,因为我不再为中缀运算符而苦苦挣扎。有两个重要的功能没有定义,所以这是我的尝试:
let logger = MyFavoriteLoggingFramework.Create()
let private funcData (expr: Expr<_->_>) =
// return a composition of [Module] and [FunctionName]
// implemantations is similar to the match in the first Logger.bind
// with slightly more complexity for the generated string
type Log<'T> =
| LogResult of ('T)
static member startAt(data, initial) =
logger.debug("")
logger.debug("logger sequence started")
LogResult <| (Ok initial, data)
static member debug([<ReflectedDefinition>]func:Expr<_->_>) =
logger.debug("")
logger.debug(funcData func)
(true, (quotationEval func))
static member (>>=) (LogResult (result, data), (_, expr:'a->'b->Result<'a,'a0>)) =
match result with
| Ok res ->
logger.debug(sprintf "\t%s: %A" "arguments" (result, data))
let ret = expr res data, data
logger.debug(sprintf "\t%s: %A" "return" ret)
LogResult <| (ret)
| Error err ->
logger { debug (sprintf "\t%s: %A" "previous error" err)}
LogResult <| (Error err, data)
static member (>>=) (LogResult (result, data), expr:'a->'b->Result<'a,'a0>) =
match result with
| Ok res -> LogResult <| (expr res data, data)
| Error err -> LogResult <| (Error err, data)
data.startAt root
>>= Logger.debug DataStructure.firstChild
>>= Logger.debug DataStructure.nextSibling
>>= DataStructure.firstChild
//
//logger sequence started
//
// executed: DataStructure.firstChild
// arguments: Ok root, [/some inmutable data structure/]
// return: Ok firstChildRoot, [/some inmutable data structure/]
//
// executed: DataStructure.nextSibling
// arguments: Ok firstChildRoot, [/some inmutable data structure/]
// return: Ok secondChildRoot, [/some inmutable data structure/]
如您所见,有两个>>=
函数,一个将 bool 和 expr 的元组作为第二个参数,另一个仅接受一个函数。拥有一个元组而不是一个函数的唯一方法是首先通过 Logger.debug 所以只有在使用 Logger 时。调试参数并打印结果。它比我想要的更详细但很明确,我更喜欢它而不是<@ ... @>
每个要记录的函数的语法。
4. 结论和帮助
好吧,如果您在这里,我要感谢您,我知道它很大,但这是解释我尝试的继承以及最终起作用的唯一方法。我还没有时间清理代码,但我想我明天可能会这样做,并将最终版本的最小清理版本上传到 gist。我想听听我错过的事情,反馈并知道是否有更好、更清晰或惯用的方法来做到这一点,尤其是利用中缀运算符(我不太明白为什么我不能让它们在这个情况,我真的觉得这里的语言很挣扎)我很想写一些类似的东西
data @> root
>>=! DataStructure.firstChild
>>=! DataStructure.nextSibling
>>= DataStructure.firstChild
// or like
data @> root
>>= log DataStructure.firstChild
>>= log DataStructure.nextSibling
>>= DataStructure.firstChild
无法得到任何这些,第一个是因为中缀运算符没有像我想象的那样工作,第二个是因为:
let log = Logger.debug
data.init root
>>= log DataStructure.firstChild
>>= DataStructure.nextSibling
>>= DataStructure.firstChild
//
//logger sequence started
//
// executed: _arg0
// arguments: Ok root, [/some inmutable data structure/]
// return: Ok firstChildRoot, [/some inmutable data structure/]
因为没有直接将参数传递给标有自动引用属性的函数。
我可能遗漏了一些非常明显的东西,所以如果是这样,请赐教,我真的很喜欢我目前正在做的事情,而且 F# 是一种非常漂亮的语言。谢谢大家,我等待您的意见;再一次,对不起我的英语^^。
编辑 1
这是最后一个版本的要点: https ://gist.github.com/rodrigo-o/1ef435887d3419be0272a5afe810f8c6