簡體   English   中英

如何在F#中編寫自己的List.map函數

[英]How to write own List.map function in F#

我必須使用'for elem in list'和tail / non-tail遞歸來編寫自己的List.map函數。 我一直在谷歌尋找一些提示,但沒有找到太多。 我已經習慣了Python,很難不考慮使用它的方法,但當然,這些語言彼此非常不同。

對於第一個我開始的東西,如:

let myMapFun funcx list =
    for elem in list do
        funcx elem::[]

尾遞歸:

let rec myMapFun2 f list =
    let cons head tail = head :: tail

但無論如何,我知道這是錯的,感覺不對。 我想我還沒有用到F#結構。 任何人都可以幫我一把嗎?

謝謝。

作為一般規則,當您在F#中處理列表時,您希望編寫一個遞歸函數,該函數對列表的頭部執行某些操作,然后在列表的尾部調用自身。 像這樣:

// NON-tail-recursive version
let rec myListFun list =
    match list with
    | [] -> valueForEmptyList  // Decision point 1
    | head :: tail ->
        let newHead = doSomethingWith head  // Decision point 2
        newHead :: (myListFun tail)  // Return value might be different, too

您需要做出兩個決定:如果列表為空,我該怎么辦? 我該怎么處理列表中的每個項目? 例如,如果您想要做的事情是計算列表中的項目數,那么您的“空列表值”可能為0,而您對每個項目執行的操作是將1添加到長度。 也就是說,

// NON-tail-recursive version of List.length
let rec myListLength list =
    match list with
    | [] -> 0  // Empty lists have length 0
    | head :: tail ->
        let headLength = 1  // The head is one item, so its "length" is 1
        headLength + (myListLength tail)

但是這個函數有一個問題,因為它會為列表中的每個項添加一個新的遞歸調用。 如果列表太長,堆棧將溢出。 一般模式,當你面對非尾遞歸的遞歸調用(比如這個)時,就是改變你的遞歸函數,以便它需要一個額外的參數,它將是一個“累加器”。 因此,不是從遞歸函數返回結果而是進行計算,而是對“累加器”值執行計算,然后在真正的尾遞歸調用中將新累加器值傳遞給遞歸函數。 這是myListLength函數的myListLength

let rec myListLength acc list =
    match list with
    | [] -> acc  // Empty list means I've finished, so return the accumulated number
    | head :: tail ->
        let headLength = 1  // The head is one item, so its "length" is 1
        myListLength (acc + headLength) tail

現在你將其稱為myListLength 0 list 由於這有點煩人,你可以通過使它成為“內部”函數中的參數來“隱藏”累加器,該函數的定義隱藏在myListLength 像這樣:

let myListLength list =
    let rec innerFun acc list =
        match list with
        | [] -> acc  // Empty list means I've finished, so return the accumulated number
        | head :: tail ->
            let headLength = 1  // The head is one item, so its "length" is 1
            innerFun (acc + headLength) tail
    innerFun 0 list

請注意myListLength不再是遞歸的,它只需要一個參數,即要計算其長度的列表。

現在回過頭來看看我在答案的第一部分中提到的泛型,非尾遞歸的myListFun 看看它如何與myListLength函數對應? 好吧,它的尾遞歸版本也很適合myListLength的尾遞歸版本:

let myListFun list =
    let rec innerFun acc list =
        match list with
        | [] -> acc  // Decision point 1: return accumulated value, or do something more?
        | head :: tail ->
            let newHead = doSomethingWith head
            innerFun (newHead :: acc) tail
    innerFun [] list

...除非你以這種方式編寫map功能,否則你會注意到它實際上已經反轉了 解決的辦法是改變innerFun [] list的最后一行到innerFun [] list |> List.rev ,但為什么它出來逆轉的原因是什么,你會從工作為自己受益,所以我不會除非你尋求幫助,否則告訴你。

現在,順便說一句,你有一個通用的模式,用遞歸的方式用列表做各種各樣的事情。 編寫List.map應該很容易。 對於額外的挑戰,請嘗試下一步編寫List.filter :它將使用相同的模式。

let myMapFun funcx list =
  [for elem in list -> funcx elem]

myMapFun ((+)1) [1;2;3]

let rec myMapFun2 f = function      // [1]
  | [] -> []                        // [2]
  | h::t -> (f h)::myMapFun f t     // [3]

myMapFun2 ((+)1) [1;2;3]            // [4]


let myMapFun3 f xs =                // [6]
  let rec g f xs=                   // [7]
    match xs with                   // [1]
    | [] -> []                      // [2]
    | h::t -> (f h)::g f t          // [3]
  g f xs
myMapFun3 ((+)1) [1;2;3]            // [4]

                                    // [5] see 6 for a comment on value Vs variable.
                                    // [8] see 8 for a comment on the top down out-of-scopeness of F#

(*參考:

慣例:我使用a,b,c等參考編號參考的不同方面

[1] roughly function is equivalent to the use of match. It's the way they do it in
    OCaml. There is no "match" in OCaml. So this is a more compatible way
    of writing functions. With function, and the style that is used here, we can shave 
    off a whole two lines from our definitions(!)  Therefore, readability is increased(!)
    If you end up writing many functions scrolling less to be on top 
    of the breadth of what is happening is more desirable than the 
    niceties of using match. "Match" can be 
    a more "rounded" form. Sometimes I've found a glitch with function. 
    I tend to change to match, when readability is better served. 
    It's a style thing.

[1b] when I discovered "function" in the F# compiler source code + it's prevalence in OCaml, 
    I was a little annoyed that it took so long to discover it + that it is deemed such an
    underground, confusing and divisive tool by our esteemed F# brethren.

[1c] "function" is arguably more flexible. You can also slot it into pipelines really 
    quickly. Whereas match requires assignment or a variable name (perhaps an argument).
    If you are into pipelines |> and <| (and cousins such as ||> etc), then you should 
    check it out.

[1d] on style, typically, (fun x->x) is the standard way, however, if you've ever 
    appreciated the way you can slot in functions from Seq, List, and Module, then it's 
    nice to skip the extra baggage. For me, function falls into this category.

[2a] "[]" is used in two ways, here. How annoying. Once it grows on you, it's cool.
    Firstly [] is an empty list. Visually, it's a list without the stuff in it 
    (like [1;2;3], etc). Left of the "->" we're in the "pattern" part of the partern 
    matching expression. So, when the input to the function (lets call it "x" to stay 
    in tune with our earliest memories of maths or "math" classes) is an empty list, 
    follow the arrow and do the statement on the right.

    Incidentally, sometimes it's really nice to skip the definition of x altogether. 
    Behold, the built in "id" identity function (essentially fun (x)->x -- ie. do nothing). 
    It's more useful than you realise, at first. I digress. 

[2b] "[]" on the right of [] means return an empty list from this code block. Match or 
    function symantics being the expression "block" in this case. Block being the same 
    meaning as you'll have come across in other languages. The difference in F#, being 
    that there's *always* a return from any expression unless you return unit which is 
    defined as (). I digress, again.

[3a] "::" is the "cons" operator. Its history goes back a long way. F# really only 
    implements two such operators (the other being append @). These operators are 
    list specific.

[3b] on the lhs of "->" we have a pattern match on a list. So the first element 
    on the lhs of :: goes into the value (h)ead, and the rest of the list, the tail,
    goes into the (t)ail value. 

[3c] Head/tail use is very specific in F#. Another language that I like a lot, has 
    a nicer terminology for obviously interesting parts of a list, but, you know, it's 
    nice to go with an opinionated simplification, sometimes.

[3d] on the rhs of the "->", the "::", surprisingly, means join a single element 
    to a list. In this case, the result of the function f or funcx.

[3e] when we are talking about lists, specifically, we're talking about a linked 
    structure with pointers behind the scenes. All we have the power to do is to 
    follow the cotton thread of pointers from structure to structure. So, with a 
    simple "match" based device, we abstract away from the messy .Value and .Next() 
    operations you may have to use in other languages (or which get hidden inside
    an enumerator -- it'd be nice to have these operators for Seq, too, but
    a Sequence could be an infinite sequence, on purpose, so these decisions for
    List make sense). It's all about increasing readability.

[3f] A list of "what". What it is is typically encoded into 't (or <T> in C#). 
    or also <T> in F#. Idiomatically, you tend to see 'someLowerCaseLetter in 
    F# a lot more. What can be nice is to pair such definitions (x:'x). 
    i.e. the value x which is of type 'x.

[4a] move verbosely, ((+)1) is equivilent to (fun x->x+1). We rely on partial
    composition, here. Although "+" is an operator, it is firstmost, also a 
    function... and functions... you get the picture.

[4b] partial composition is a topic that is more useful than it sounds, too.

[5] value Vs variable. As an often stated goal, we aim to have values that 
    never ever change, because, when a value doesn't change, it's easier to 
    think and reason about. There are nice side-effects that flow from that 
    choice, that mean that threading and locking are a lot simpler. Now we 
    get into that "stateless" topic. More often than not, a value is all you
    need. So, "value" it is for our cannon regarding sensible defaults.

    A variable, implies, that it can be changed. Not strictly true, but in
    the programming world this is the additional meaning that has been strapped 
    on to the notion of variable. Upon hearing the word variable, ones mind might
    start jumping through the different kinds of variable "hoops". It's more stuff 
    that you need to hold in the context of your mind. Apparently, western people 
    are only able to hold about 7 things in their minds at once. Introduce mutability 
    and value in the same context, and there goes two slots. I'm told that more uniform
    languages like Chinese allow you to hold up to 10 things in your mind at once. 
    I can't verify the latter. I have a language with warlike Saxon and elegant 
    French blended together to use (which I love for other reasons).

    Anyway, when I hear "value", I feel peace. That can only mean one thing.

[6] this variation really only achieves hiding of the recursive function. Perhaps
    it's nice to be a little terser inside the function, and more descriptive to 
    the outside world. Long names lead to bloat. Sometimes, it's just simpler.

[7a] type inference and recursion. F# is one of the nicest 
    languages that I've come across for elegantly dealing with recursive algorithms.
    Initially, it's confusing, but once you get past that

[7b] If you are interested in solving real problems, forget about "tail" 
    recursion, for now. It's a cool compiler trick. When you get performance conscious, 
    or on a rainy day, it 
    might be a useful thing to look up.
    Look it up by all means if you are curious, though. If you are writing recursive 
    stuff, just be aware that the compiler geeks have you covered (sometimes), and 
    that horrible "recursive" performance hole (that is often associated with 
    recursive techniques -- ie. perhaps avoid at all costs in ancient programming 
    history) may just be turned into a regular loop for you, gratis. This auto-to-loop 
    conversion has always been a compiler geek promise. You can rely on it more though. 
    It's more predictable in F# as to when "tail recursion" kicks in. I digress. 
    Step 1 correctly and elegantly solve useful problems. 
    Step 2 (or 3, etc) work out why the silicon is getting hot.

    NB. depending on the context, performance may be an equally important thing
    to think about. Many don't have that problem. Bear in mind that by writing 
    functionally, you are structuring solutions in such a way that they are 
    more easily streamlineable (in the cycling sense). So... it's okay not to
    get caught in the weeds. Probably best for another discussion.

[8] on the way the file system is top down and the way code is top down. 
    From day one we are encouraged in an opinionated (some might say coerced) into
    writing code that has flow + code that is readable and easier to navigate.
    There are some nice side-effects from this friendly coercion.

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM