Arc Forumnew | comments | leaders | submitlogin
Noobing with news.arc
2 points by markkat 5130 days ago | 26 comments
Hi. I've set out to teach myself some programming, and I've decided to start off by playing with Arc, and news.arc in particular. My previous programming experience is with FORTRAN more than 10 years ago. I've built some basic websites in the interim.

I've deployed a news forum on linode, and for the last couple of days have been tweaking it, getting frustrated, and generally having a lot of fun.

Here's a question: I'd like to add the functionality that when a user logs in, and it is a new day, the user's karma is ++.

How might I go about it? ...and maybe a bit of 'why' I might go about it that way too.

Thanks!



4 points by fallintothis 5130 days ago | link

BIG WARNING: I've never really run, read, or modified news.arc thoroughly. This is as much as I can gather from just looking.

There are several ways you might login in news.arc, and correspondingly, there are specific bits of code for each of those cases: when you click the top-right login link (see topright in news.arc), when you try to vote (see newsop vote), when you try to submit (see submit-login-warning), when you try to comment (see comment-login-warning), when you try to reply (see newsop reply), etc.

What do these have in common? The login-page function, defined in app.arc:

  (def login-page (switch (o msg nil) (o afterward hello-page))
    (whitepage
      (pagemessage msg)
      (when (in switch 'login 'both)
        (login-form "Login" switch login-handler afterward)
        (hook 'login-form afterward)
        (br2))
      (when (in switch 'register 'both)
        (login-form "Create Account" switch create-handler afterward))))
In each context, different things happen with the afterward parameter passed to login-page. topright logs that the user signed in via the topright link, newsop vote registers the user's vote after they log in, submit-login-warning redirects the user to the submit page, etc. Since login-page is the single point of entry, you might change it. However, there's this comment in news.arc:

  ; Need this because can create users on the server (for other apps)
  ; without setting up places to store their state as news users.
  ; See the admin op in app.arc.  So all calls to login-page from the 
  ; news app need to call this in the after-login fn.

  (def ensure-news-user (u)
    (if (profile u) u (init-user u)))
Aha. This whittles down a single-point of entry specifically for news.arc logins, since they all need to call ensure-news-user in the afterwards parameter of login-page. So, it makes more sense to change this.

You still need to make sure ensure-news-user behaves the same way (i.e., returns the proper user) so that the functions that use ensure-news-user don't break. Shouldn't be too bad to add a ++ in there, then just return the user.

I'll interpret "it is a new day" as "it's a new day between now and when they last logged in" (which isn't necessarily 24 hours, just a new date). This requires knowing when they last logged in. Probably the easiest way to get this would be adding some new info to the profile template, and recording it on each login (an action we're doing now anyways, so not a big deal). Look for deftem profile and add a line to it:

  (deftem profile
    id         nil
    name       nil
    created    (seconds)
    ...
    last-login (date) ; default last-login = whenever the new profile was made
    ...
    delay      0)
Now all you need to do is record their last login in ensure-news-user and do some date arithmetic to see if it's a "new day".

  ; These two functions take in the today argument (i.e., (date)), since
  ; otherwise you might do the equivalent of
  ;
  ;   (it-is-a-new-day u (date))
  ;   ...
  ;   (update-last-login u (date))
  ;
  ; But (date) might change between the times when you make the two function
  ; calls, so it should be bound outside, like
  ;
  ;   (let today (date)
  ;     (it-is-a-new-day u today)
  ;     ...
  ;     (update-last-login u today))

  (def it-is-a-new-day (u today)
    (with ((new-y new-m new-d) today
           (old-y old-m old-d) (uvar u last-login))
      (or (> new-d old-d)                   ; e.g., Apr 15, 2010 -> Apr 16, 2010
          (> new-m old-m)                   ; e.g., Apr 30, 2010 -> May 01, 2010
          (> new-y old-y))))                ; e.g., Dec 31, 2010 -> Jan 01, 2011

  (def update-last-login (u today)
    (= (uvar u last-login) today))

  (let old ensure-news-user
    (def ensure-news-user (u)
      (with (u (old u) today (date))
        (when (it-is-a-new-day u today)
          (++ (karma u)))
        (update-last-login u today))
        u)) ; return the result of old-ensure-news-user
Per my warning, this is untested. Someone else is probably more qualified to gauge the quality of this response. Alternatively, you could increase everyone's karma as a batch job that runs every midnight, but that removes the login part of your requirement. Good luck!

-----

3 points by shader 5130 days ago | link

Just one question: Is their karma supposed to be increased every time the log in and it's a new day, or every time they are logged in and it's a new day? I ask because I almost never have to log in to the arc forum; my session apparently lasts a long time, possibly lasting until I log in from a different computer or longer. As such, I never actually go through the official log in process, but I'm still online.

Does ensure-news-user get run on page load as part of the code that checks whether their logged in or not? From a cursory look at the code, it didn't appear to work that way.

How would you write it differently if you wanted to increase their karma for every day they logged on, as opposed to for day they logged in?

-----

2 points by akkartik 5130 days ago | link

I was wondering about that as well. Perhaps it makes sense to insert a hook (http://files.arcfn.com/doc/variables.html#defhook) into get-user (top of app.arc) rather than ensure-news-user.

You'd be adding some overhead to every single request. Not a big deal to begin with, but something to keep in mind.

-----

3 points by thaddeus 5130 days ago | link

Great response - as always!

the date comparison could be easier:

  (deftem profile
      ...
      last-login (datestring))

   (let old ensure-news-user
      (def ensure-news-user (u)
        (with (u (old u) today (datestring))
          (when (> today (uvar u last-login))
            (++ (karma u)))
          (update-last-login u today))
          u))

-----

1 point by akkartik 5130 days ago | link

Since time always increases, you can also just:

  (unless (is (date) (uvar u last-login))
    (++ karma.u)
    (= (uvar u last-login) (date)))
update: Hmm, I just realized there's a few places I can use this macro:

  (mac updating(place expr . body)
    (w/uniq rhs
      `(let ,rhs ,expr
         (unless (is ,place ,rhs)
           (= ,place ,rhs)
           ,@body))))

  ..
  (updating (uvar u last-login) (date)
    (++ karma.u))
Perhaps we need to make the is parameterizable as well in the general case. Hmm, this could be a generalization of my firsttime (http://arclanguage.org/item?id=12889)

  (mac updating(place ? expr t iff 'is . body)
    (w/uniq rhs
      `(let ,rhs ,expr
         (unless (,iff ,place ,rhs)
           (= ,place ,rhs)
           ,@body))))

  (mac firsttime(place . body)
    `(updating ,place
        :body
          ,@body))
(requires my keyword args and new optional syntax: http://github.com/akkartik/arc)

Now you can run several kinds of test-and-update:

  (firsttime user!loggedin
     (prn *welcome-message*))

  (updating (uvar u last-login) (date)
    :body
      (++ karma.u))

  ; track largest value seen so far
  (ret max 0
    (each elem '(1 3 2 6 5)
      (updating max :iff > elem
        (prn "max now: " max))))

  max now: 1
  max now: 3
  max now: 6
  6
Hmm, I might get rid of firsttime altogether. Is updating the right name for this macro?

-----

2 points by rocketnia 5130 days ago | link

For this:

  (mac updating(place ? expr t iff 'is . body)
    (w/uniq rhs
      `(let ,rhs ,expr
         (unless (,iff ,place ,rhs)
           (= ,place ,rhs)
           ,@body))))
You're evaluating the subexpressions of 'place twice, which isn't necessary thanks to 'setforms. Also, I like to have macros macro-expand and evaluate their parameters from left to right if I can, just for similarity with 'do. (I still try to evaluate only as much as I need to though.) To accomplish these things, I'd go with this (untested) implementation instead:

  (def fn-updating (current-val setter-getter new-val comparator body)
    (unless (do.comparator current-val new-val)
      (.new-val:do.setter-getter)
      (do.body)))
  
  (mac updating (place ? expr t iff 'is . body)
    (let (binds val setter) setforms.place
      `(withs ,binds
         (fn-updating ,val (fn () ,setter) ,expr ,iff (fn () ,@body)))))
In the meantime, for the purposes of dates, I'd at least use 'iso instead of 'is. You're probably not going to get the same (date) list as one you have from earlier today. Even if (date) itself made that guarantee, you've potentially persisted and un-persisted the user data today.

In fact, since you've already made 'iso extensible, I'd use it as the default instead of 'is.

-----

1 point by akkartik 5130 days ago | link

All great points, thanks! I have in fact stopped using is everywhere in my code, don't know what I was thinking.

-----

1 point by akkartik 5129 days ago | link

Why the do's?

And what does the .new-val do?

-----

3 points by rocketnia 5129 days ago | link

Saying (do.foo ...) instead of (foo ...) just makes it so that 'foo isn't in function position. That means the local variable 'foo will always be used instead of some macro named 'foo that happened to be defined in another library.

The ssyntaxes .foo and !foo are short for get.foo and get!foo. Also, a:b ssyntax is handled before a.b ssyntax, giving the : lower precedence.

That means (.new-val:do.setter-getter) compiles as though it's ((get new-val) ((do setter-getter))), which makes it a roundabout way to accomplish ((do.setter-getter) new-val) with one less pair of parentheses, one less character, or (by my count) one less token.

It's really sort of silly in this case, since you save on so little while rearranging so much. In its favor, he (.a:b ...) idiom makes a bit more sense when it's a field/element access like (!width:get-rect ...) or (!2:setforms ...). It's especially helpful when it's part of a big chain of composed forms like (.a:b:.c:.d:e ...).

In my own code, including Lathe, I define this:

  (def call (x . args)
    (apply x args))
Then the code is just call.setter-getter.new-val, giving a savings of two characters, two pairs of parentheses, or (by my count) four tokens, all without causing the expressions to be rearranged.

Seems like a lot of brevity boilerplate, huh? :-p

-----

2 points by aw 5129 days ago | link

Saying (do.foo ...) instead of (foo ...) just makes it so that 'foo isn't in function position. That means the local variable 'foo will always be used instead of some macro named 'foo that happened to be defined in another library.

I remembered talking about this once and found the comment: http://arclanguage.org/item?id=11697

It looks like it would be an easy change to Arc to have local variables take precedence, though I haven't tried the patch myself.

-----

1 point by akkartik 5129 days ago | link

Oh nice. So is there a way using this trick to do

  a."b".c.d.g
without parens?

-----

3 points by rocketnia 5129 days ago | link

Not in Arc. The best you get is (.g:.d:.c:a "b"), I think. You can shove "b" into ssyntax with string!b, but you need to use it as an argument somehow, and any ssyntax containing .string!b will pass the 'string function itself as an argument instead of calling it first.

Of course, if "b" were a symbol instead of a string, it would just be "a!b.c.d.g".

In Penknife, the answer to your question is actually yes: It's "q.b'a.c.d.g", where q is the string-quoting operator and ' is an operator that acts like a reverse a.b. It's easy to stump Penknife too, but I'm hoping to make a full thread about Penknife's take on infix operators in a couple of days or so.

-----

2 points by thaddeus 5126 days ago | link

Optionally, in ac.scm:

  (define (has-ssyntax-char? string i)
    (and (>= i 0)
         (or (let ((c (string-ref string i)))
               (or (eqv? c #\:) (eqv? c #\~) 
                   (eqv? c #\&)                
                   (eqv? c #\%)                
                   ;(eqv? c #\_) 
                   (eqv? c #\.)(eqv? c #\!)))
             (has-ssyntax-char? string (- i 1)))))

  (define (expand-ssyntax sym)
    ((cond ((or (insym? #\: sym) (insym? #\~ sym)) expand-compose)
           ((or (insym? #\. sym) (insym? #\! sym)(insym? #\% sym)) expand-sexpr)
           ((insym? #\& sym) expand-and)
       ;   ((insym? #\_ sym) expand-curry)
           (#t (error "Unknown ssyntax" sym)))
   sym))

  (define (build-sexpr toks orig)
    (cond ((null? toks)
           'get)	    
          ((null? (cdr toks))
           (chars->value (car toks)))
          (#t
           (list (build-sexpr (cddr toks) orig)
                 (cond ((eqv? (cadr toks) #\!)
                        (list 'quote (chars->value (car toks))))
                       ((eqv? (cadr toks) #\%)
                         (list 'string (list 'quote (chars->value (car toks)))))
                       ((or (eqv? (car toks) #\.)(eqv? (car toks) #\!)(eqv? (car toks) #\$)(eqv? (car toks) #\%))
                         (err "Bad ssyntax" orig))
                       (#t 
                       (chars->value (car toks))))))))
Then:

  arc> (= test (obj "a" "my a val" "b" "my b val"))
  #hash(("a" . "my a val") ("b" . "my b val"))

  arc> test%a
  "my a val"
[edit: actually, it would be nicer to have the percent symbol represent the spaces in the string and have some other symbol signify string handling, but I never got around to it + my scheme foo is lacking :)]

-----

1 point by akkartik 5129 days ago | link

Is fn-updating generally useful?

-----

2 points by markkat 5130 days ago | link

Wow. Thanks for such a thorough response! Funny, I had 'last-log' in the deftem profile, and was tweaking ensure-news-user. I couldn't get it working, but's nice to know I was tweaking in the right places. BTW, your interpretation of what I meant by 'new day' was spot on.

I am going to spend some time trying this out. Thank you. I'll let you know how it goes.

-----

1 point by markkat 5130 days ago | link

Thanks again. Based on the login issue shader pointed out, I decided to change things up a bit, and ++ karma if it is a new day, and the user comments or replies. This is what I did, and it seems to work. I started with process-comment:

  (deftem profile
  ...
  karmadate nil
  ...
  )

  (def process-comment (user parent text ip whence)
  ...
         (day-karma user)
         whence)))

  (def day-karma (u)
    (when (~iso (date) (uvar u karmadate))
      (++ (karma u))
    (update-karmadate u)))

  (def update-karmadate (u)
    (= (uvar u karmadate) (date))
    (save-prof u))
But now looking at akkartik's

  (unless (is (date) (uvar u last-login))
    (++ karma.u)
    (= (uvar u last-login) (date)))
Maybe I'll go for that. BTW, karma.u what's up with the . ?

-----

3 points by shader 5130 days ago | link

Arc has a feature called "ssyntax", short for symbol syntax, that allows us to use certain characters in a symbol as shorthand for a commonly used longer form.

In this case, "karma.u" expands to (karma u), which is a macro that looks up the users karma. Functions applied to single arguments are extremely common in arc, so it is quite useful to have an abbreviation.

Other ssyntax include:

  a!b -> (a 'b)
  ~a -> (no a)
  a~b -> (compose a (no b))
  a:b -> (compose a b) ;(a:b c) is equivalent to (a (b c))

-----

1 point by markkat 5129 days ago | link

Ah... thanks. Great way to throw noobs like me off though. :) I was wondering about the colon too, I couldn't find an explanation anywhere.

-----

1 point by akkartik 5130 days ago | link

Don't forget to replace is with iso like rocketnia pointed out.

-----

1 point by markkat 5129 days ago | link

Will do. Thanks for that.

-----

1 point by markkat 5127 days ago | link

I wasn't sure if this warranted a new submission, so I am just adding it here for now:

Continuing my news.arc mods, I am currently trying to create a 'hover-over' effect on the .comhead that isn't just text-style, but pulls up the top comment in a popup. For example, ATM, the comment of fallintothis would popup onmouseover on the mainpage. I might be over my head here yet, but any pointers would be appreciated.

-----

1 point by rocketnia 5126 days ago | link

It sounds like you're talking about what forums usually accomplish using the HTML "title" attribute. Doing it in JavaScript lets your bubble be more stylish, and there might even be a way to do stylish bubbles using just CSS (having a separate :hover style or something, with the regular style hiding the children), but "title" should be the easiest to try first:

  <a href="http:..." title="Agreed.">Ask so-and-so: ...</a>
Something you might find convenient here is Arc's 'ellipsize function, which shortens long strings and puts ... at the end.

-----

1 point by markkat 5126 days ago | link

Thanks. Struggling with it, but I'll keep at it. Will title really work if I am referencing a call to comment text? I'm going to give the :hover route a shot. Not too worried about style though. :)

Ellipsize will definitely come in handy. Thanks for the tip.

-----

1 point by markkat 5124 days ago | link

Hm. This is breaking my brain. I can create a td class for comments that enables an independent hover effect, but I get a large space in the preceding subtext class cell, which I can't seem to resolve.

Using spanclass the spacing is conserved, but I only get some attributes onto the comment, but not hover. Here's where I am working, with a spanclass comref that only seems to be able to override the color of the subtext class.

  (def commentlink (i user)
    (when (cansee user i) 
      (pr bar*)
      (tag (a href (item-url i!id))
        (let n (- (visible-family user i) 1)
          (if (> n 0)
              (spanclass comref
              (do (pr (plural n "comment"))
                  (awhen (and show-threadavg* (admin user) (threadavg i))
                    (pr " (@(num it 1 t t))"))))
              (pr "Add comment"))))))
I tried to define an ID class, but #class doesn't seem to work as a declaration? Not sure if it is a failing of my CSS, Arc, or both. :/

If anyone has an idea how to create a hover effect on comments when n<0, please throw me a bone. :)

-----

2 points by akkartik 5126 days ago | link

One suggestion: position the popup to the right of the main panel (like on google search)

-----

1 point by markkat 5125 days ago | link

That is a great idea. I have no clue how to do it, but I am trying... Like I said, I am a noob.

I often find myself clicking through to comments on HN and reddit, just curious to see what the highest rated one is. -I thought it be nice to be able to save the trip.

I've set out to refashion news.arc in my own way, and teach myself some programming along the way. I'll share it once I'm done. So far I am enjoying it. Arc is a bit terse, but clear once I can get a handle on a concept. Slowly, I am getting familiar with it. I've been able to modify karma from a point system into a type of currency on my own, and I am pretty happy at that.

-----