F# 和 ADO.NET - F#的惯用方式

21

我刚开始学习F#,昨晚写了这段F# / ADO.NET代码。你会如何改进语法-使其感觉像习惯用语的F#?

    let cn = new OleDbConnection(cnstr)
    let sql = "SELECT * FROM People"
    let da = new OleDbDataAdapter(new OleDbCommand(sql, cn))
    let ds = new DataSet()
    cn.Open()
    let i = da.Fill(ds)
    let rowCol = ds.Tables.[0].Rows
    let rowCount = rowCol.Count
    printfn "%A" rowCount

    for i in 0 .. (rowCount - 1) do
        let row:DataRow = rowCol.[i]
        printfn "%A" row.["LastName"]

注意: 我发现语法检查器不喜欢rowCol.[i].["LastName"]的语法。如何正确处理双索引器?我不得不将代码拆成两行。

还有 如果我没有使用DataSet路线,而是使用将数据加载到F#记录中的SqlDataReader。那么应该使用什么集合结构来包含这些记录?标准的.NET List<>?


我不确定我理解这个问题。 - BuddyJoe
1
他的意思是手头的任务(使用.NET库进行数据库操作)必然会以命令式代码结束,因此F#在这方面并不突出。但是一旦你将数据从数据库中取出来,它可能非常适合处理数据。 - Mau
@tyndall Mau 是完全正确的。你的代码是100%命令式的。你可以使用查找替换将其转换为C#。我认为F#可能不是最好的工具。 - Andrey
13
@Andrey:在这方面,F#与C#完全相同。问题是,如果您想写一些优雅的F#代码,您需要先获取数据 :-). - Tomas Petricek
1
我正在尝试提取数据,然后利用F#的优势。但是我希望避免使用C#和F#编写应用程序。 - BuddyJoe
3个回答

33

你的代码中涉及到了一个不太实用的 .NET API,所以没有办法使这部分代码特别符合惯用语言或更加优雅。然而,在函数式编程中,关键在于抽象,因此你可以将这个(丑陋的)代码隐藏在某个符合惯用语言且可重复使用的函数中。

在 F# 中表示数据集合时,你可以使用标准的 F# list 类型(适用于函数式数据处理),或者seq<'a>(它是标准的 .NET IEnumerable<'a> 库,可与其他 .NET 库良好协作)。

根据你在代码中的数据库访问方式,以下方法可能有效:

// Runs the specified query 'sql' and formats rows using function 'f'
let query sql f = 
  // Return a sequence of values formatted using function 'f'
  seq { use cn = new OleDbConnection(cnstr) // will be disposed 
        let da = new OleDbDataAdapter(new OleDbCommand(sql, cn)) 
        let ds = new DataSet() 
        cn.Open() 
        let i = da.Fill(ds) 
        // Iterate over rows and format each row
        let rowCol = ds.Tables.[0].Rows 
        for i in 0 .. (rowCount - 1) do 
            yield f (rowCol.[i]) }

现在你可以使用query函数,将你的原始代码大致编写如下:

let names = query "SELECT * FROM People" (fun row -> row.["LastName"])
printfn "count = %d" (Seq.count names)
for name in names do printfn "%A" name

// Using 'Seq.iter' makes the code maybe nicer 
// (but that's a personal preference):
names |> Seq.iter (printfn "%A")

另一个可以编写的示例是:

// Using records to store the data
type Person { LastName : string; FirstName : string }
let ppl = query "SELECT * FROM People" (fun row -> 
  { FirstName = row.["FirstName"]; LastName = row.["LastName"]; })

let johns = ppl |> Seq.filter (fun p -> p.FirstName = "John")

顺便说一句,关于Mau的建议,如果有更直接的方法可以使用语言结构(如for),我不会过度使用高阶函数。上面用iter的例子足够简单,有些人可能会觉得更易读,但没有通用的规则...


1
如果我正确理解了你的第一个例子,seq { }会导致再次查询数据库,对吗?而你的“let ppl =”这一行避免了可变性问题,因为它将其设置为序列。我走在正确的道路上吗? - BuddyJoe
1
@tyndall:这是一个很好的观点!每次您使用该序列(例如使用Seq.countfor)时,序列将被重新评估(并再次查询数据库)。这有点不幸,而 let ppl =.. 也无法避免这种情况。但是,您可以编写 let ppl = ... |> Seq.cache 或者例如 List.ofSeq 来运行查询并将结果作为列表获取。 - Tomas Petricek
我喜欢List.ofSeq的想法。+1 - BuddyJoe
“for”循环可以简化为以下代码,用于生成序列:for row in rowCol do yield f row. 这样我们就不需要变量i了,可以使用“da.Fill(ds) |> ignore”。 - Stephen Hosking
很好的回答。有点吹毛求疵,但我认为更好的选择是避免通过.Fill()急切地加载结果,并通过IDbReader慢慢消耗它。 - pim

7
我写了一个ADO.NET的F#函数式封装库。使用这个库后,你的示例代码看起来像这样:
let openConn() =
   let cn = new OleDbConnection(cnstr)
   cn.Open()
   cn :> IDbConnection

let query sql = Sql.execReader (Sql.withNewConnection openConn) sql

let people = query "select * from people" |> List.ofDataReader
printfn "%d" people.Length
people |> Seq.iter (fun r -> printfn "%s" (r?LastName).Value)

4

嗯,第一部分你无法更改太多内容,但是每当您处理数据集合时,例如在最后几行中,您可以使用内置的Seq、List、Array函数。

for i in 0 .. (rowCount - 1) do
  let row:DataRow = rowCol.[i]
  printfn "%A" row.["LastName"]

=

rowCol |> Seq.cast<DataRow> 
       |> Seq.iter (fun row -> printfn "%A" row.["LastName"])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接