F#编程练习

12

我一直在Visual Studio 2010中尝试F#。作为一个有着C#和Java等面向对象语言的更多代码/架构设计经验的开发人员,我想拓展我的技能并尝试使用不同的语言来做不同的事情。特别是要通过使用函数式语言(在这种情况下是F#)正确编写代码。

一个简单的例子是生成一些XML,然后添加一些过滤器以消除某些元素。

这是我的代码:

open System
open System.Xml.Linq


let ppl:(string * string) list = [
    ("1", "Jerry"); 
    ("2", "Max"); 
    ("3", "Andrew");
]

/// Generates a Person XML Element, given a tuple.
let createPerson (id:string, name:string) = new XElement(XName.Get("Person"),
                                                new XAttribute(XName.Get("ID"), id),
                                                new XElement(XName.Get("Name"), name)
)

/// Filter People by having odd ID's
let oddFilter = fun (id:string, name:string) -> (System.Int32.Parse(id) % 2).Equals(1) 

/// Open filter which will return all people
let allFilter = fun (id:string, name:string) -> true

/// Generates a People XML Element.
let createPeople filter = new XElement(XName.Get("People"), 
                                ppl |> List.filter(filter)  |> List.map createPerson
)

/// First XML Object
let XmlA = createPeople oddFilter

/// Second XML Object
let XmlB = createPeople allFilter


printf "%A\n\n%A" XmlA XmlB


/// Waits for a keypress
let pauseKey = fun () -> System.Console.ReadKey() |> ignore


pauseKey()
我的问题是:在这种情况下,我做得好的事情有哪些?有哪些部分可以做得更好?
我真的很期待一些想法,也很兴奋能够熟悉函数式范例! :)
提前感谢。

我在完全相同的情况下(但显然是不同的代码),很想看到这个问题的答案。 - Jay
1
F# 的基本设计思想是为您计算类型,它具有非常强大的推断引擎,因此您应该习惯省略类型注释,并尽可能将函数定义为通用函数。 - 0xFF
@martain_net,谢谢你的建议,这真的很有道理 :) 我想我过度使用了类型定义来理解不同的本地 F# 类型(例如 (string * string) list 表示包含两个字符串元组的列表)。 - Russell
4个回答

13

原则上,你的代码是正确的。

只是从语法角度来看,有一些可以简化的地方。

let ppl:(string * string) list = [
    ("1", "Jerry"); 
    ("2", "Max"); 
    ("3", "Andrew");
]

编译器可以自己推断大多数类型:

let ppl = [ "1", "Jerry";
            "2", "Max";
            "3", "Andrew" ]

当然,你也可以通过柯里化来重写过滤器:

let oddFilter (id:string, name:string) = (int id) % 2 = 1
let allFilter (id:string, name:string) = true

最大的改进将是将索引与名称分离,并让程序进行编号。您无需使用字符串而不是数字,可以使用更具惯用性的无元组函数:

let ppl = [ "Jerry"; "Max"; "Andrew" ]

let oddFilter id name = id % 2 = 1
let allFilter id name = true

let createPerson id name = ...

这部分

ppl |> List.filter(filter)  |> List.map createPerson

会被重写为

[ for (index, name) in List.mapi (fun i x -> (i, x)) do
      if filter index name then
          yield createPerson (string index) name ]

2
是的,类型推断和柯里化。好东西。 - cfern
谢谢Dario,有一些好的想法。你介意我问一下为什么你要重写ppl |> List.filter(filter) |> List.map createPerson吗?我对处理过程有点困惑。看起来有一些主题我会去研究。:) 再次感谢 - Russell
重点是只处理真正需要它们的索引。因此,我在createPeople内部生成它们。 - Dario
5
List.choose (item -> if filter item then Some (createPerson item) else None) 也可以工作 :) Choose 相当于同时使用 filter 和 map。 - Juliet
1
因为 let f = fun x -> ...let f x = ... 是一样的。 - Dario
显示剩余2条评论

4
let createPeople filter = new XElement(XName.Get("People"), 
                            ppl |> List.filter(filter)  |> List.map createPerson
)

这部分内容可以手动进行deforest,或者您可以希望编译器为您进行deforest。

基本上,存在一个中间结构(过滤人员列表),如果以天真的方式编译,则将仅分配一次以提供服务。最好在确定他们是否在内部时对每个元素应用createPerson,并直接构建最终结果。

编辑: cfern贡献了createPeople的这个经过deforested处理的版本:

let createPeople filter = 
  new XElement(
    XName.Get("People"), 
    List.foldBack 
      (fun P acc -> if filter P then (createPerson P)::acc else acc) 
      ppl 
      [])

注意:由于在filtercreatePerson中可能存在副作用,因此在F#中编译器很难自行决定是否进行deforesting。 在这种情况下,我认为进行deforesting是正确的,因为即使filter具有副作用,createPerson也没有,但我不是专家。


2
我现在无法测试,但您能否将List.filter和List.map替换为Seq.filter和Seq.map以避免创建中间列表? - cfern
1
啊,我明白了。我还是新手函数式编程,因此还没有完全习惯折叠操作。这就是你所想的吗?...让createPeople filter = new XElement(XName.Get("People"), List.foldBack (fun P acc -> if filter P then (createPerson P)::acc else acc) ppl []) ...在这里我使用foldBack来保留人员的顺序。 - cfern
谢谢你的建议,Pascal。我会研究一下deforesting(这个词用得对吗?)和foldBack。看起来有一些List方法我需要熟悉一下。 :) - Russell
嗯,别这样 - 初看时没有人能理解这个。请为此编写一个新的函数 filterMap - Dario
2
@Dario 1) 没有人会明白什么?2) 正如我之前所说,我不够自信写F#。3) 这个答案是社区维基,因为cfern提出了实际的替代实现,所以请随意。如果你在谈论那个实现,我理解了,而且我甚至不知道F#! - Pascal Cuoq
显示剩余2条评论

3

大多数情况下,没有特定原因(通常是性能),砍伐森林都是一个坏主意。哪种方式更易于阅读并且不容易出错?如果脱离上下文的话,砍伐森林只会给你的代码增加复杂性和/或耦合。

let createPeople filter ppl =
    ppl 
    |> List.mapi (fun i x -> (i, x)) 
    |> List.filter filter
    |> List.map createPerson

let createPeople filter ppl =
    [ for (index, name) in ppl |> List.mapi (fun i x -> (i, x)) do
          if filter (index, name) then
              yield createPerson (index, string) ]

let createPeople filter ppl = 
    (ppl |> List.mapi (fun i x -> (i, x)), []) 
    ||> List.foldBack (fun P acc -> if filter P then (createPerson P)::acc else acc) 

一旦你习惯了语法,函数组合可以让你摆脱繁琐的编码。

let createPeople filter =
    List.mapi (fun i x -> (i, x)) 
    >> List.filter filter
    >> List.map createPerson

所有这些都使用元组数据。
let filter (id, name) = 
    id % 2 = 1

let allFilter (id, name) = 
    true

let createPerson (id, name) = 
    ()

谢谢Gradbot。出于好奇,F#中的>>是什么意思?这是一个像|>?一样的管道运算符吗?还是一个位运算符(将位向右移动)? - Russell
它将两个函数连接在一起,左边的函数先被调用。它的定义是 let (>>) f g x = g(f x) - TimothyP

0

我最近也需要将XSL转换为XML文件。这是我使用的F#代码。

在使用.net方法时有一些有趣的小问题。

(* Transforms an XML document given an XSLT. *)

open System.IO
open System.Text
open System.Xml
open System.Xml.Xsl

let path = @"C:\\XSL\\"

let file_xml = path + "test.xml"
let file_xsl = path + "xml-to-xhtml.xsl"

(* Compile XSL file to allow transforms *)
let compile_xsl (xsl_file:string) = new XslCompiledTransform() |> (fun compiled -> compiled.Load(xsl_file); compiled)
let load_xml (xml_file:string) = new XmlDocument() |> (fun doc -> doc.Load(xml_file); doc)

(* Transform an Xml document given an XSL (compiled *)
let transform (xsl_file:string) (xml_file:string) = 
      new MemoryStream()
        |> (fun mem -> (compile_xsl xsl_file).Transform((load_xml xml_file), new XmlTextWriter(mem, Encoding.UTF8)); mem)
        |> (fun mem -> mem.Position <- (int64)0; mem.ToArray())

(* Return an Xml fo document that has been transformed *)
transform file_xsl file_xml
    |> (fun bytes -> File.WriteAllBytes(path + "out.html", bytes))

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