Arc Forumnew | comments | leaders | submit | akkartik's commentslogin
2 points by akkartik 2506 days ago | link | parent | on: Knark - rewrite in plain Racket?

Can you elaborate on what extra features Clojure's tables provide?

-----

4 points by i4cu 2506 days ago | link

Where to start...

1. The ability to read them. Especially on output. It may seem small, but when you have a larger deeply nested map it really matters.

  {:user "akkartik"}
2. Keywords are ifns that work on hash-maps.

  (map :user list-of-maps)
3. All the built in library fns that work on them. Basics + Assoc-in, Get-in, merge, deep-merge, contains?..

Here's a good function to separate a collection of maps by any fn/ifn.

  (def separate (juxt filter remove))
 
  (separate :user data)

4. Iteration just works, ie they automatically become association lists for pretty much every composing fn (reduce, loop, ect)

5. Destructuring bind. Eg:

  (defn myfn [{:keys [user] :as record}]
    (str user " rocks"))
6. Nil values are supported.

It goes on, but I think that's enough... Besides my thumbs are getting sore typing via phone.

-----

3 points by akkartik 2506 days ago | link

You'll need to elaborate more, apologies in advance to your thumbs. Some of the features you describe I don't understand (ability to read, maybe others). Others seem to already be in Arc (library support, iteration, destructuring bind; [_ 'user] doesn't seem much worse than :user). Nil values feels like the only definitely missing feature, and it feels more like a fork in the road with different trade-offs rather than one alternative being always superior to the other.

-----

7 points by i4cu 2506 days ago | link

Ok so let's use this data

Clojure:

  > (def players
       {:joe {:class "Ranger" :score 100}
        :jane {:class "Knight" :weapon "Greatsword" :score 140}
        :ryan {:class "Wizard" :weapon "Mystic Staff" :score 150}})
response:

{:joe {:class "Ranger" :score 100} :jane {:class "Knight" :weapon "Greatsword" :score 140} :ryan {:class "Wizard" :weapon "Mystic Staff" :score 150}})

Arc:

  (= players
    (obj 'joe (obj 'class "Ranger" 'score 100)
         'jane  (obj 'class "Knight" 'weapon "Greatsword" 'score 140)
         'ryan  (obj 'class "Wizard" 'weapon "Mystic Staff" 'score 150))) 
response:

#hash(((quote jane) . #hash(((quote weapon) . "Greatsword") ((quote class) . "Knight") ((quote score) . 140))) ((quote ryan) . #hash(((quote weapon) . "Mystic Staff") ((quote class) . "Wizard") ((quote score) . 150))) ((quote joe) . #hash(((quote class) . "Ranger") ((quote score) . 100))))

-- Readability --

Reading that output is beyond a headache. It should be against the law! But more importantly, when writing code/troubleshooting you can't copy the evaluated output make a modification and then call the function again. I know you can bind it and apply functions to modify it, but often that's a real hassle compared to copy/paste.

-- Destructuring bind --

See links [a,b]

I think simple cases don't show the difference.

  > (def book {:name "SICP" :details {:pages 657 :isbn-10 "0262011530"}})
  
  > (let [{name :name {pages :pages isbn-10 :isbn-10} :details} book]
      (println "name:" name "pages:" pages "isbn-10:" isbn-10))
	  
response:

    name: SICP pages: 657 isbn-10: 0262011530
	
-- Library functions --

Clojure has DOZEN's of helper functions (and important traversal functions) that don't exist in Arc. Yeah I could write them, but I still have to. I was highlighting the ease with 'separate', but here's even 'deep-merge'....

  (defn deep-merge
    "Recursively merges maps. If keys are not maps, the last value wins."
    [& vals]
     (if (every? map? vals)
         (apply merge-with deep-merge vals)
         (last vals)))
		 
  > (deep-merge players {:joe {:weapon "LongBow"} :jane {:weapon "Lance"}})
  
It's not even these sample cases that expose the issues. It's about working with REAL data. I have many files containing hash-maps that are 15 levels deep. If I were to attempt pretty much anything, but the most simple cases, in Arc I would hit problem after problem after problem. Which I did. So fine... really I should use a DB, right? ok let me just grab that DB library in Arc....

-- Nil values --

I'd even be fine with any falsey values like boolean false. But Arc will not even store an #f value. To suggest I can't pass in REAL data with falsey values is really limiting. I can't pass in an options table argument. For every operation I would have to add more data or nuanced data or nuanced operations to support "this value was set, but it's set to 'NO' I don't want that option.".

[a] https://clojure.org/guides/destructuring#_associative_destru...

[b] https://clojure.org/guides/destructuring#_keyword_arguments

-----

3 points by akkartik 2505 days ago | link

Thanks! That's very helpful.

-----

4 points by akkartik 2518 days ago | link | parent | on: Tell Arc: Arc 3.2

Thanks so much, Scott!

-----

4 points by akkartik 2542 days ago | link | parent | on: Recursive anonymous functions?

Is that the Y combinator? I don't think I've seen it ever used "for real". It's not in either Arc 3.1 or Anarki.

You aren't using bracket notation as you originally asked. Might as well just use afn. It can call itself recursively as self. http://arclanguage.github.io/ref/anaphoric.html

-----

3 points by waterhouse 2538 days ago | link

The Y combinator itself is more cumbersome, having an extra currying step or two. I prefer the form hjek is using—which is a function that expects to take "itself" as an extra parameter, like this:

  (fn (f i)
    (aif i!parent
         (+ 1 (f f (item it)))
         0))
So the recursive call, "(<self> (item it))", is implemented as "(f f (item it))". And then usage is very simple: actually give it itself as an extra argument.

The Y combinator works with a different function signature:

  (fn (f)
    (fn (i)
      (aif i!parent
           (+ 1 (f (item it)))
           0)))
That is, the function takes "something that's not quite itself" as an argument, and returns a function which does one step of computation and may do a "recursive" call using the thing that was passed into it. The implementation would therefore like to be:

  (def fix (f) ;aka Y
    (fn (i)
      ((f (fix f)) i)))
But, if we're doing the entire thing with anonymous recursion, we can (laboriously) implement fix like this:

  (= fix ;aka Y
     (fn (f)
       ((fn (g) (g g))
        (fn (g)
          (fn (i)
            ((f (g g)) i))))))
Every recursion step involves creating multiple lambdas. Eek. (It's even worse if you use the general, n-argument Y combinator, in which case you must use "apply" and create lists.) Whereas with hjek's non-curried approach, only a constant number of lambdas have to be created at runtime. (Optimizing compilers might be able to cut it down to 0.)

If you want to create a macro like afn or rfn, and want the user to be able to act like the function is named F and accepts just the parameter i, you can put a wrapper into the macroexpansion, like this:

  (rfn F (i)
    (aif i!parent
         (+ 1 (F (item it)))
         0))
  ->
  (fn (i)
    ((fn (f)
       (f f i))
     (fn (f i)
       (let F (fn (i) (f f i))
         (aif i!parent
              (+ 1 (F (item it)))
              0)))))
And in this case, while the code does call for creating an F-lambda on every recursive call, I think it's easier for the compiler to eliminate it—I don't remember whether I'd gotten Racket to do it. (I think it probably did eliminate it when working with Racket code, but Arc, which generates all the ar-funcall expressions, might not have allowed that.)

The actual code for rfn will create a variable and then modify it, creating a lexical environment with a cycle in it. That's certainly a more straightforward approach. I figure the above is useful only if you're working in a context where you really want to avoid mutation or true cycles. (For example, I am considering a system that detects macros whose expansion is completely side-effect-free. It might be easier to use the above approach to defining iteration than to teach the system that rfn is "close enough" to being side-effect-free.)

-----

2 points by hjek 2539 days ago | link

I've been using `aif` and `awhen` a lot but didn't know about `afn`. Thanks!

Is it the Y combinator? I didn't get that far in The Little Schemer yet, but I'll have to check.

-----

2 points by akkartik 2539 days ago | link

Doesn't look quite like it, but close.

-----

2 points by akkartik 2545 days ago | link | parent | on: Writing a sane list macro

That seems pretty much unhacky :)

-----


Check out John Shutt's thesis on Kernel: https://web.cs.wpi.edu/~jshutt/kernel.html. He takes your observation that quasiquotation is pointless to the limit, dropping it entirely and constructing his macros out of cons and friends.

-----

2 points by waterhouse 2538 days ago | link

Thanks for the pointer. He has an entire chapter on hygiene... And then there is this:

"There is no need to provide quotation here because, having failed to enforce the prohibition against embedding combiners in a macro expansion, we don’t need to embed their unevaluated names in the expansion."

It's nice that his primitive $vau grabs the current lexenv, as this enables another kind of macro-like behavior I've thought might be useful: taking subexpressions, fully macroexpanding them, and doing something with the result (e.g. determining whether a variable is ever used, and omitting a computation if not). I don't know how that would mesh with the interpreter's semantics, though...

I'll probably have to read and brood on this further.

-----

3 points by akkartik 2554 days ago | link | parent | on: About lobste.rs

I'll invite you.

-----

3 points by zck 2553 days ago | link

Thanks! I'll spend some time checking it out. :)

-----

2 points by akkartik 2556 days ago | link | parent | on: Self-hosting the Anarki community

Hmm, I wonder if it stays banned until they restart the server. I'd ping hn@ycombinator.com.

-----

2 points by hjek 2555 days ago | link

Looks like banned IPs are written to the disk even:

    (def set-ip-ban (user ip yesno (o info))
      (= (banned-ips* ip) (and yesno (list user (seconds) info)))
      (todisk banned-ips*))

-----

4 points by akkartik 2563 days ago | link | parent | on: With and withs

I actually tend to the opposite: use with everywhere unless I need withs. The reason isn't performance. It tends to make code more elegant to not rely on the order in which things are defined. And when I'm reading code, with gives me the warm fuzzies that the code is going to be cleaner. When I see withs I slow down to look at the dependencies between the bindings.

-----

3 points by zck 2563 days ago | link

Similarly to this, when I'm writing Java, I use `final`^1 everywhere I can. It's nice to be able to know that anywhere later where the variable declared final is in scope, it will have the same value as at the point it's set. I don't need to look through any code to see if it's rebound; I know it hasn't been.

[1] "final" is kind of like "const", if I understand `const` right. `final int x = 3;` means that it is an error to later have the line of code `x = 4;`.

-----

3 points by prestonbriggs 2563 days ago | link

OK, I get it, thanks. In scheme, I would use letrec for this situation; my intuition for Arc isn't very well developed.

-----

3 points by akkartik 2563 days ago | link | parent | on: With and withs

Try running without your code.

    arc> (help get)
    [fn]  (get i)
    Returns a function to pass 'i' to its input.
    Useful in higher-order functions, or to index into lists, strings, tables, etc.
    Examples:
      arc> (get.2 '(1 2 3 4))
      3
      arc> (get!b (obj a 10 b 20))
      20
      arc> (get.9 sqrt)
      3
      arc> (map get.2
                '((a b c)
                  (1 2 3)
                  (p q r)))
      (c 3 r)
    nil

    arc> (help set)
    [mac] (set . args)
    Sets each place in 'args' to t.
    nil
These are the functions you end up calling because your dispatch can't see the earlier get and set bindings.

-----

2 points by akkartik 2565 days ago | link | parent | on: Self-hosting the Anarki community

I did ping the HN admins about the lock period a year or two ago, and they were kind enough to extend it for us. It's now 90 days, if I recall correctly.

-----

More