{"id":15994236,"url":"https://github.com/knutwalker/scala-orbit","last_synced_at":"2025-08-15T22:43:53.185Z","repository":{"id":17751228,"uuid":"20600631","full_name":"knutwalker/scala-orbit","owner":"knutwalker","description":"Orbital simulator in Scala","archived":false,"fork":false,"pushed_at":"2014-06-09T16:59:58.000Z","size":6900,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-02T13:25:59.395Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/knutwalker.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-06-07T19:09:28.000Z","updated_at":"2014-06-10T07:01:07.000Z","dependencies_parsed_at":"2022-09-24T18:42:44.235Z","dependency_job_id":null,"html_url":"https://github.com/knutwalker/scala-orbit","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knutwalker%2Fscala-orbit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knutwalker%2Fscala-orbit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knutwalker%2Fscala-orbit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/knutwalker%2Fscala-orbit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/knutwalker","download_url":"https://codeload.github.com/knutwalker/scala-orbit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247266570,"owners_count":20910837,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-08T07:07:33.798Z","updated_at":"2025-04-05T00:15:45.423Z","avatar_url":"https://github.com/knutwalker.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"Scala orbit playground\n======================\n\nA Scala port of Uncle Bobs [clojureOrbit](https://github.com/unclebob/clojureOrbit) as he [presented it to the Chicago JUG](https://www.youtube.com/watch?v=SYeDxWKftfA).\n\n\nThis is mostly for fun, to see how certain idiomatic Clojure would translate to Scala.\nSometimes, a more Clojure-esque Scala is written (e.g. using stuff `-\u003e\u003e` und `@`), and sometimes, it's the other way around (e.g. using `isCollide` instead of `collide?`).\n\n## Usage\n\nThis project uses [`sbt`](http://www.scala-sbt.org/).\n\n### run\n\n```\n./sbt run\n```\n\n### test\n\n```\n./sbt test\n```\n\n### build uberjar\n```\n./sbt assembly\njava -jar target/scala-2.11/scala-orbit-assembly-1.0.jar\n```\n\n### develop\n\nRun this in one terminal\n\n```\n./sbt ~test\n```\n\nIn another terminal, just enter the sbt shell\n\n```\n./sbt\n```\n\nAnd use the task `re-start` if you want to run the GUI.\n\nWith this setup you have:\n\n1. a forked VM for every start, so you safely kill it without getting thrown out of the sbt shell\n2. automatic test execution on every file change. code -\u003e save -\u003e see tests fail (or pass)\n\n\n## Differences from Uncle Bob\n\n### Changes in code style\n\nI tried to mirror Uncle Bobs code as closely as possible, that includes variable und function names with these exceptions (and possible some others):\n- Where Clojure would use `make`, the Scala idiomatic naming is `apply`\n- Predicates, that end in Clojure on `?`, e.g. `foo?` have the Scala (Java) naming of `isFoo`\n- hyphen-case names are replaced by camelCase names, although it would be possible in Scala, to used hyphen-cased names as well\n\n### Changes in domain types\n\n- Used `Vec` instead of `vector`, `Pos` instead of `position`, and `Obj` instead of `object`\n- Used value classes and type aliases for stuff like the age of an collision, the delay setting, the history of worlds, etc.\n    Since Scala is statically typed, using small domain types like this can increase readability, the ability to reason about the code, or the amount of support we get from the compiler.\n    On top, Scala makes it very easy to create such types and properly used value class have no runtime overhead, they exist only during compile time.\n\n\n### Changes in implementation\n\n#### less recur\n\nI replaced some recursive loops with higher kinded loops from the Scala collection library.\nFor example, Uncle Bob writes:\n\n```clojure\n(defn accumulate-forces\n  ([o world]\n   (assoc o :force (accumulate-forces o world (vector/make))))\n  ([o world f]\n   (cond\n     (empty? world) f\n     (= o (first world)) (recur o (rest world) f)\n     :else (recur o (rest world) (vector/add f (force-between o (first world)))))))\n```\n\nHere, I used a fold instead:\n\n```scala\ndef accumulateForces(o: Obj, world: World): Obj = {\n  val newForce = world.foldLeft(Vec()) { (f, obj) =\u003e\n    if (obj == o) f else Vec.add(f, forceBetween(o, obj)) }\n  o.copy(force = newForce)\n}\n```\n\nAnother example from Uncle Bob\n\n```clojure\n(loop [world [sun] n 400]\n  (if (zero? n)\n    world\n    (recur (conj world (random-object sun n)) (dec n))))\n```\n\nwhich I replaced with\n\n```scala\nsun +: Vector.tabulate(400)(n =\u003e randomObject(sun, 400 - n))\n```\n\n#### other stuff\n\n- I did not check the current `controls`, if a `mouseup` is set to determine when to trigger the `handle-mouse` handler but used monadic comprehension for this.\n    I always call `handleMouse`, but it would only execute the state changes, if both mouse events are set.\n- For the history/trails, Uncle Bob used a `vector` and ~~adds~~ `conj`s new worlds to the end of it.\n    I used a `List` instead and `cons` new world to the beginning.\n    I could have used a `Vector` and appending as well, but prepending to Lists is such a typically found Scala usage, that I didn't wanted to discard it.\n    There might be other places, where I used a List instead of a Vector.\n\n### Changes in behavior\n\nThe general intention (entites) of the program did not change, but I modified some stuff on the UI plugin.\n- For the trails, I actually just used a simple 'least x worlds' approach. Uncle Bob did something, that would delete some random trails\n- Magnifying the UI does not center the view around the sun\n\n### Additions\n\n- Mapped the arrow keys, so that you can move around\n - press shift for faster pace\n - switched from `KeyEvent.getKeyCode` to `getKeyChar` for this\n - moving disabled the sun tracing, since that would always reset your viewport to the suns center\n- Added a mapping to `r`, that would remove all but the current world, thus eliminating the trails\n- Added command line options\n - `-sun` sets the initial mass of the sun (default: 1500)\n - `-count` sets the initial object count (default: 400)\n- The color of an object gets less and less opaque, the more it gets to the tail end. So, the current objects have full opacity, the least recent object is almost fully transparent. This makes the trails fade out nicely.\n\n\n## Conclusion\n\nClojure and Scala are not so different. This is mostly due to the functional nature of the program.\nOf course you can have Scala and Clojure code that differ tremendously and especially Scala code, that is not functional at all.\nBut, if you know your way around functional programming, the difference between Scala and Clojure becomes more and more just syntax (and a bit of types). Using higher order functions, dealing with state and immutability, recursion etc. is all very similar (though, Scala _does_ have tail recursion).\nA typical landmark of functional code is, that functions tend to have a bunch of assignment statements and then few, ideally just one actual thing they do.\nThis looks quite good in Clojure with `let`.\n\n```clojure\n(defn foo [a b]\n  (let [c (:bar a)\n        d (:baz b)\n        e (qux/quaz c d)]\n    (+ 23 42 1337)))\n```\n\nWhereas in Scala, this would only be a convention.\n\n```scala\ndef foo(a: Any, b: Any) = {\n  val c = a.bar\n  val d = b.baz\n  val e = qux.quaz(c, d)\n  23 + 42 + 1337\n}\n```\n\n\n### Atoms\n\n[`atom`s](http://clojure.org/atoms) seam really nice. However, Scala does not have atoms, so I kinda had to [implement](src/main/scala/orbit/atom.scala#L5) them myself.\nBut since atoms are quite simple, this was an easy task. Given their [Clojure implementation](https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Atom.java), I'm not that far off.\n\n### doto\n\n[`doto`](http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/doto) is another nice thing for Java interop (especially with the [GUI stuff](https://github.com/unclebob/clojureOrbit/blob/master/src/orbit/world.clj#L235-L243)).\nThere is no direct Scala equivalent, but due to the way Scala handle blocks and imports, it can be [simulated very easily](/src/main/scala/orbit/package.scala#L238-L246):\n\n```clojure\n(doto (new java.util.HashMap) (.put \"a\" 1) (.put \"b\" 2))\n```\n\nbecomes\n\n```scala\nval m = new java.util.HashMap\n\n{ import m._\n  put(\"a\", 1)\n  put(\"b\", 2) }\n```\n\nOk, not _really_ the same, but close enough.\n\n\n### vec\n\nThere's some weird stuff happening with [`vec`](http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/vec).\nOr I don't quite understand it yet, whatever is more likely.\nI choose to ignore this and instead stick with ye good olde Scala collections.\n\n\n### Similarities\n\nSome aspects seem quite different at first glance, but are actually very similar.\n\n#### assoc\n\nClojures `assoc`\n\n```clojure\n(assoc o :position (position/add p v))\n```\n\nand Scalas `copy` on `case class`es\n\n```scala\no.copy(pos = Pos.add(p, v))\n```\n\nEven the number of parens are the same :-)\n\n\n#### destructuring\n\nClojure can destructure parameters\n\n```clojure\n(defn merge [{n1 :name, m1 :mass, v1 :velocity f1 :force, :as o1}\n             {n2 :name, m2 :mass, v2 :velocity f2 :force, :as o2}]\n  ...)\n```\n\nScala uses pattern matching for destructuring\n\n```scala\ndef merge(o1: Obj, o2: Obj): Obj = {\n  val (Obj(_,  m1, v1, f1, n1), Obj(_,  m2, v2, f2, n2)) = (o1, o2)\n  ...\n}\n````\n\n#### loops\n\nClojure with overloaded methods and recur\n\n```clojure\n(defn accumulate-forces\n  ([o world]\n   (assoc o :force (accumulate-forces o world (vector/make))))\n  ([o world f]\n   (cond\n     (empty? world) f\n     (= o (first world)) (recur o (rest world) f)\n     :else (recur o (rest world) (vector/add f (force-between o (first world)))))))\n```\n\nScala idiomatic code uses nested tail recursion for this\n\n```scala\ndef accumulateForces(o: Obj, world: World): Obj = {\n  def recur(o: Obj, world: World, f: Vec): Vec =\n    if (world.isEmpty) f\n    else if (world.head == o) recur(o, world.tail, f)\n    else recur(o, world.tail, Vec.add(f, forceBetween(o, world.head)))\n  o.copy(force = recur(o, world, Vec()))\n}\n```\n\n\n### Conciseness\n\nScala claims to allow you to write concise and expressive code. Clojure even more so.\nThere's even a [Twitter account](https://twitter.com/learnclojure) that teaches you Clojure in parts of 140 characters.\n\nHowever, these are the (totally scientific) line counts.\n\nComplete project including test sources and everything:\n\n```\n∵ cloc clojureOrbit/src/ src/\n      26 text files.\n      26 unique files.\n       0 files ignored.\n\nhttp://cloc.sourceforge.net v 1.60  T=0.09 s (290.2 files/s, 15852.0 lines/s)\n-------------------------------------------------------------------------------\nLanguage                     files          blank        comment           code\n-------------------------------------------------------------------------------\nClojure                         13            112              0            600\nScala                           13            129              0            579\n-------------------------------------------------------------------------------\nSUM:                            26            241              0           1179\n-------------------------------------------------------------------------------\n```\n\nOnly the relevant source files, sans tests and atom:\n\n```\n∵ cloc --not-match-f='atom.scala|.*(_test|-tests).clj' clojureOrbit/src/ src/main/\n      14 text files.\n      14 unique files.\n       0 files ignored.\n\nhttp://cloc.sourceforge.net v 1.60  T=0.06 s (232.4 files/s, 14854.8 lines/s)\n-------------------------------------------------------------------------------\nLanguage                     files          blank        comment           code\n-------------------------------------------------------------------------------\nScala                            6             82              0            372\nClojure                          8             75              0            366\n-------------------------------------------------------------------------------\nSUM:                            14            157              0            738\n-------------------------------------------------------------------------------\n```\n\nAnd only the relevant entities, sans the UI stuff:\n\n```\n∵ cloc --not-match-f='.*(_test|-tests).clj' clojureOrbit/src/physics/ src/main/scala/physics/\n       9 text files.\n       9 unique files.\n       0 files ignored.\n\nhttp://cloc.sourceforge.net v 1.60  T=0.03 s (273.6 files/s, 9790.3 lines/s)\n-------------------------------------------------------------------------------\nLanguage                     files          blank        comment           code\n-------------------------------------------------------------------------------\nClojure                          5             36              0            136\nScala                            4             29              0            121\n-------------------------------------------------------------------------------\nSUM:                             9             65              0            257\n-------------------------------------------------------------------------------\n```\n\nSo, Scala and Clojure appear to be quite similar in this regard.\nNow, just counting lines is a silly measure for the conciseness of the code, but it's a start. I'd suspect different numbers if I were to compare Scala against, say, Java ... as I said, totally scientific.\n\n\n### Wrap up\n\nAnyway, this was fun and I got to learn a bit Clojure on the way. Who knows, maybe, some day, I'll port some of my Scala stuff to Clojure :-)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fknutwalker%2Fscala-orbit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fknutwalker%2Fscala-orbit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fknutwalker%2Fscala-orbit/lists"}