[英]Mapping my code from OCaml/F# to Scala - some questions
我在業余時間學習Scala-作為一項學習練習,我將在另一個StackOverflow問題中編寫的 OCaml代碼翻譯為Scala。 由於我是Scala的新手,所以我希望您能提供一些建議...
但是在問我的問題之前,這是原始的OCaml代碼:
let visited = Hashtbl.create 200000
let rec walk xx yy =
let addDigits number =
let rec sumInner n soFar =
match n with
| x when x<10 -> soFar+x
| x -> sumInner (n/10) (soFar + n mod 10) in
sumInner number 0 in
let rec innerWalk (totalSoFar,listOfPointsToVisit) =
match listOfPointsToVisit with
| [] -> totalSoFar
| _ ->
innerWalk (
listOfPointsToVisit
(* remove points that we've already seen *)
|> List.filter (fun (x,y) ->
match Hashtbl.mem visited (x,y) with
| true -> false (* remove *)
| _ -> (Hashtbl.add visited (x,y) 1 ; true))
(* increase totalSoFar and add neighbours to list *)
|> List.fold_left
(fun (sum,newlist) (x,y) ->
match (addDigits x)+(addDigits y) with
| n when n<26 ->
(sum+1,(x+1,y)::(x-1,y)::(x,y+1)::(x,y-1)::newlist)
| n -> (sum,newlist))
(totalSoFar,[])) in
innerWalk (0,[(xx,yy)])
let _ =
Printf.printf "Points: %d\n" (walk 1000 1000)
...這是我將其翻譯為的Scala代碼:
import scala.collection.mutable.HashMap
val visited = new HashMap[(Int,Int), Int]
def addDigits(number:Int) = {
def sumInner(n:Int, soFar:Int):Int =
if (n<10)
soFar+n
else
sumInner(n/10, soFar+n%10)
sumInner(number, 0)
}
def walk(xx:Int, yy:Int) = {
def innerWalk(totalSoFar:Int, listOfPointsToVisit:List[(Int,Int)]):Int = {
if (listOfPointsToVisit.isEmpty) totalSoFar
else {
val newStep =
listOfPointsToVisit.
// remove points that we've already seen
filter(tupleCoords => {
if (visited.contains(tupleCoords))
false
else {
visited(tupleCoords)=1
true
}
}).
// increase totalSoFar and add neighbours to list
foldLeft( (totalSoFar,List[(Int,Int)]()) )( (state,coords) => {
val (sum,newlist) = state
val (x,y) = coords
if (addDigits(x)+addDigits(y) < 26)
(sum+1,(x+1,y)::(x-1,y)::(x,y+1)::(x,y-1)::newlist)
else
(sum,newlist)
});
innerWalk(newStep._1, newStep._2)
}
}
innerWalk(0, List((xx,yy)))
}
println("Points: " + walk(1000,1000))
Scala代碼可以編譯並正常工作,並報告正確的結果。
但...
除非我錯過了什么,否則我在Scala中找不到管道運算符(即OCaml和F#的|>
),因此我使用了相應的列表方法( filter
和fold Left
)。 在這種情況下,最終結果與原始結果非常接近,但是我想知道-管道運算符不是功能性解決方案的總體上有利的且更為通用的方法嗎? 為什么Scala沒有配備?
在Scala中,我必須使用特定於類型的空列表專門初始化折疊狀態(這是(Int, List[Int,Int])
的元組。用簡單的話來說, List()
並沒有剪切它-我有明確指定List[(Int,Int)]()
,否則我收到一條...相當困難的錯誤消息。我根據上下文對其進行了解密-它抱怨Nothing
-並且我意識到此小代碼中唯一的地方類型Nothing
出現可能是我的空列表。但是,與OCaml相比,結果更丑陋……我能做些什么更好的選擇?
同樣,OCaml能夠將折疊的結果元組作為innerWalk
的參數innerWalk
。 在Scala中,我必須分配一個變量並使用innerWalk(newStep._1, newStep._2)
調用尾遞歸調用。 元組和函數參數之間似乎不存在等價關系-即我無法在具有兩個參數的函數中傳遞2圓度的元組-類似地,我無法將函數的參數元組分解為變量(我必須在折疊功能的主體內明確分配state
和coords
並對其進行coords
,我是否缺少某些東西?
總體而言,我對結果感到滿意-我想說的是,如果我們將本示例的OCaml代碼分級為100%,則Scala約為85-90%-比OCaml更為冗長,但要多得多比OCaml更接近Java。 我只是想知道我是否充分利用了Scala,或者是否錯過了一些可以改善代碼的構造(更有可能)。
請注意,我避免將原始OCaml的模式匹配映射到Scala的模式,因為在這種情況下,我認為這是過分的-兩個地方的if
表達式都更加清晰。
在此先感謝您的幫助/建議。
PS附注:我在walk
呼叫周圍添加了時序說明(從而避免了JVM的啟動成本),並測量了我的Scala代碼-它的運行速度約為OCaml的50%-有趣的是,它的速度與我完全相同退出Mono執行F#等效代碼(如果您關心這種比較,請參閱我最初的SO問題以獲取F#代碼)。 由於我目前在企業環境中工作,因此我很樂意付出50%的速度來編寫簡潔的類似ML的代碼, 並且仍然可以訪問廣大的JVM / .NET生態系統(數據庫,Excel文件生成等)。 。 抱歉,OCaml,我確實嘗試過-但您無法完全“說出” Oracle :-)
編輯1 :經過@senia和@lmm的善意建議后,代碼得到了顯着改進 。 希望從@lmm獲得更多關於foldMap和Shapeless如何提供幫助的建議:-)
編輯2 :我在這里用scalaz- gist的 flatMap進一步清除了代碼。 不幸的是,此更改還導致了10倍的大幅下降-猜測foldMap完成的列表串聯比foldLeft的“僅添加一個新節點”要慢得多。 想知道如何更改代碼以快速添加內容...
編輯3 :在@lmm的另一個建議之后,我將scalaz-flatMap版本從使用List
切換為使用immutable.Vector :這很有幫助,將速度從原來的10倍降低到...僅慢2倍(比原始代碼慢2倍) )。 那么,干凈的代碼還是2倍的速度? 決定,決定... :-)
|>
運算符,或者您可以自己編寫一個。 通常,在Scala中對它的需求要少得多,因為對象具有方法,如您在某些翻譯中所看到的(例如, somethingThatReturnsList.filter(...)
,在OCaml中,您必須編寫somethingThatReturnsList |> List.filter(...)
。因此它不是語言的內置語言,但是如果您需要它,它就在那里。 foldLeft
有點一般; 您可能能夠使用例如Scalaz foldMap
編寫更清晰的代碼(在您的元組的情況下,您可能還需要shapeless-contrib以便派生適當的typeclass實例)。 但是從根本上說是肯定的,Scala類型推斷將不如OCaml可靠,並且您將發現自己必須添加顯式類型注釋(有時是因為不清楚Nothing
錯誤消息)-這是我們允許傳統OO extends
繼承的代價。 (innerWalk _).tupled
獲得帶元組的函數。 或者,您可以編寫函數來接受元組,並利用參數自動組合的優勢來調用它們而無需顯式的元組語法。 但是,是的,沒有多參數函數的通用編碼(您可以使用Shapeless將它們轉換為該形式),我主要是因為JVM的兼容性。 我懷疑如果現在編寫標准庫,它將對所有內容使用HList
,並且普通函數和HList
表示形式之間是等效的,但這將是很難以向后兼容的方式進行的更改。 您似乎在使用大量的if
s,並且有一些函數可以用於您正在執行的某些操作,例如, visited.put(tupleCoords, 1)
返回一個布爾值,用於確定是否替換了一個值,因此可以將其用作filter
調用的整個過程。 就像我說的,如果您願意使用Scalaz,則foldLeft
可以重寫為更清晰的foldMap
。 我懷疑整個遞歸循環都可以用命名結構來表示,但是沒有立即想到的東西,所以也許沒有。
我附加了兩個替代版本以及更多慣用的Scala代碼(我還對該算法進行了一些優化)。 在這種情況下,我認為命令性解決方案沒有任何問題,實際上它更容易理解。
// Impure functional version
def walk2(xx: Int, yy: Int) = {
val visited = new mutable.HashSet[(Int, Int)]
def innerWalk(totalSoFar: Int, listOfPointsToVisit: Seq[(Int, Int)]): Int = {
if (listOfPointsToVisit.isEmpty) totalSoFar
else {
val newStep = listOfPointsToVisit.foldLeft((totalSoFar, Seq[(Int, Int)]())) {
case ((sum, newlist), tupleCoords@(x, y)) =>
if (visited.add(tupleCoords) && addDigits(x) + addDigits(y) < 26)
(sum + 1, (x + 1, y) +: (x - 1, y) +: (x, y + 1) +: (x, y - 1) +: newlist)
else
(sum, newlist)
}
innerWalk(newStep._1, newStep._2)
}
}
innerWalk(0, Seq((xx, yy)))
}
// Imperative version, probably fastest
def walk3(xx: Int, yy: Int) = {
val visited = new mutable.HashSet[(Int, Int)]()
val toVisit = new mutable.Queue[(Int, Int)]()
def add(x: Int, y: Int) {
val tupleCoords = (x, y)
if (visited.add(tupleCoords) && addDigits(x) + addDigits(y) < 26)
toVisit += tupleCoords
}
add(xx, yy)
var count = 0
while (!toVisit.isEmpty) {
count += 1
val (x, y) = toVisit.dequeue()
add(x + 1, y)
add(x - 1, y)
add(x, y + 1)
add(x, y - 1)
}
count
}
編輯:改進的功能版本,在命令版本中使用Queue
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.