- Factor
- Roc
- Nim
- Zsh
- Execline
Well FWIW CodeWars has plenty of Factor katas, and I try to gather related resources at https://programming.dev/c/concatenative
I’m trying to keep up with the Perl Weekly Challenges, but with Factor, and am posting some Factor solutions to Exercism’s 48in24 series.
Two books that may be helpful:
I’m more familiar with the former, and think it’s very good, but it may not give you the basic introduction to object oriented programming (classes and all that) you’re looking for; the latter should.
Great! If you get a chance, I’d be interested to hear about your rtx complaints.
I’m not who you asked, and not a user of pkgx, but one of the reasons I prefer rtx (which supports asdf plugins) to asdf is that by default it doesn’t use shims, but updates the PATH instead.
So for example, running which python
will show me a clearly versioned python executable path, rather than a mysterious shim that represents a different realpath at different times.
No Zsh support for now, and maybe no user fonts?
And a warning: it’s got telemetry on by default.
Factor on github (with comments and imports):
Note: lemmy mangles the double-ampersands and the less-than symbols.
: card-key ( ch -- n ) "23456789TJQKA" index ;
: five-kind? ( hand -- ? ) cardinality 1 = ;
: four-kind? ( hand -- ? ) sorted-histogram last last 4 = ;
: full-house? ( hand -- ? ) sorted-histogram { [ last last 3 = ] [ length 2 = ] } && ;
: three-kind? ( hand -- ? ) sorted-histogram { [ last last 3 = ] [ length 3 = ] } && ;
: two-pair? ( hand -- ? ) sorted-histogram { [ last last 2 = ] [ length 3 = ] } && ;
: one-pair? ( hand -- ? ) sorted-histogram { [ last last 2 = ] [ length 4 = ] } && ;
: high-card? ( hand -- ? ) cardinality 5 = ;
: type-key ( hand -- n )
[ 0 ] dip
{ [ high-card? ] [ one-pair? ] [ two-pair? ] [ three-kind? ] [ full-house? ] [ four-kind? ] [ five-kind? ] }
[ dup empty? ] [
unclip pick swap call( h -- ? )
[ drop f ] [ [ 1 + ] 2dip ] if
] until 2drop
;
:: (hand-compare) ( hand1 hand2 type-key-quot card-key-quot -- <=> )
hand1 hand2 type-key-quot compare
dup +eq+ = [
drop hand1 hand2 [ card-key-quot compare ] { } 2map-as
{ +eq+ } without ?first
dup [ drop +eq+ ] unless
] when
; inline
: hand-compare ( hand1 hand2 -- <=> ) [ type-key ] [ card-key ] (hand-compare) ;
: input>hand-bids ( -- hand-bids )
"vocab:aoc-2023/day07/input.txt" utf8 file-lines
[ " " split1 string>number 2array ] map
;
: solve ( hand-compare-quot -- )
'[ [ first ] bi@ @ ] input>hand-bids swap sort-with
[ 1 + swap last * ] map-index sum .
; inline
: part1 ( -- ) [ hand-compare ] solve ;
: card-key-wilds ( ch -- n ) "J23456789TQKA" index ;
: type-key-wilds ( hand -- n )
[ type-key ] [ "J" within length ] bi
2array {
{ { 0 1 } [ 1 ] }
{ { 1 1 } [ 3 ] } { { 1 2 } [ 3 ] }
{ { 2 1 } [ 4 ] } { { 2 2 } [ 5 ] }
{ { 3 1 } [ 5 ] } { { 3 3 } [ 5 ] }
{ { 4 2 } [ 6 ] } { { 4 3 } [ 6 ] }
{ { 5 1 } [ 6 ] } { { 5 4 } [ 6 ] }
[ first ]
} case
;
: hand-compare-wilds ( hand1 hand2 -- <=> ) [ type-key-wilds ] [ card-key-wilds ] (hand-compare) ;
: part2 ( -- ) [ hand-compare-wilds ] solve ;
Factor on github (with comments and imports):
I didn’t use any math smarts.
: input>data ( -- races )
"vocab:aoc-2023/day06/input.txt" utf8 file-lines
[ ": " split harvest rest [ string>number ] map ] map
first2 zip
;
: go ( press-ms total-time -- distance )
over - *
;
: beats-record? ( press-ms race -- ? )
[ first go ] [ last ] bi >
;
: ways-to-beat ( race -- n )
dup first [1..b)
[
over beats-record?
] map [ ] count nip
;
: part1 ( -- )
input>data [ ways-to-beat ] map-product .
;
: input>big-race ( -- race )
"vocab:aoc-2023/day06/input.txt" utf8 file-lines
[ ":" split1 nip " " without string>number ] map
;
: part2 ( -- )
input>big-race ways-to-beat .
;
Aside from the general conciseness, the “universal function call syntax” is my favorite aspect of nim.
If you want to take chaining procedures to the next level, try a concatenative language like Factor (I have a day 4 solution in this thread – with no assignment to variables).
I also suggest having a look at Roc if you want a functional programming adventure, which offers great chaining syntax, a very friendly community, and is in an exciting development phase.
Factor on github (with comments):
USING: io.encodings.utf8 io.files kernel math math.parser
prettyprint ranges sequences sets splitting ;
IN: aoc-2023.day04
: line>cards ( line -- winning-nums player-nums )
":|" split rest
[
[ CHAR: space = ] trim
split-words harvest [ string>number ] map
] map first2
;
: points ( winning-nums player-nums -- n )
intersect length
dup 0 > [ 1 - 2^ ] when
;
: part1 ( -- )
"vocab:aoc-2023/day04/input.txt" utf8 file-lines
[ line>cards points ] map-sum .
;
: follow-card ( i commons -- n )
[ 1 ] 2dip
2dup nth swapd
over + (a..b]
[ over follow-card ] map-sum
nip +
;
: part2 ( -- )
"vocab:aoc-2023/day04/input.txt" utf8 file-lines
[ line>cards intersect length ] map
dup length swap '[ _ follow-card ]
map-sum .
;
Lemmy mangles the line in part2
that starts with dup length
Factor on github:
USING: arrays io.encodings.utf8 io.files kernel make math
math.intervals math.parser prettyprint sequences
sequences.extras splitting unicode ;
IN: aoc-2023.day03
: symbol-indices ( line -- seq ) ! "*..$.*..7."
[ ".0123456789" member? not ] find-all [ first ] map ! { 0 3 5 }
;
: num-spans ( line -- seq ) ! ".664.598.."
>array [ over digit? [ nip ] [ 2drop f ] if ] map-index ! { f 1 2 3 f 5 7 }
{ f } split harvest ! { { 1 2 3 } { 5 7 } }
[ [ first ] [ last ] bi 2array ] map ! { { 1 3 } { 5 7 } }
;
: adjacent? ( num-span symbol-indices -- ? ) ! { 1 3 } { 3 5 7 }
swap [ first 1 - ] [ last 1 + ] bi [a,b] ! { 3 5 7 } [0,4]
'[ _ interval-contains? ] any? ! t
;
: part-numbers ( line nearby-symbol-indices -- seq ) ! ".664.598.." { 0 5 6 }
[ dup num-spans ] dip ! ".664.598.." { { 1 3 } { 5 7 } } { 0 5 6 }
'[ _ adjacent? ] filter ! ".664.598.." { { 1 3 } { 5 7 } }
swap '[ first2 1 + _ subseq string>number ] map ! { 664 598 }
;
: part1 ( -- )
"vocab:aoc-2023/day03/input.txt" utf8 file-lines ! lines
[ [ symbol-indices ] map ] keep ! lines-symbol-idxs lines
[ ! lines-symbol-idxs line line#
pick swap [ 1 - ?nth-of ] [ nth-of ] [ 1 + ?nth-of ] 2tri ! lines-symbol-idxs line prev-sym-idxs cur-sym-idxs next-sym-idxs
3append part-numbers sum ! lines-symbol-idxs line-parts-sum
] map-index sum nip . ! total .
;
: star-indices ( line -- seq ) ! ".*.$.*...*"
[ CHAR: * = ] find-all [ first ] map ! { 1 5 9 }
;
: gears ( line prev-line next-line -- seq-of-pairs ) ! ".*.$*.*..." ".........." ".664.598..455"
pick star-indices ! ".*.$*.*..." ".........." ".664.598..455" { 1 4 6 }
[ 1array '[ _ part-numbers ] [ 3dup ] dip tri@ 3append ] ! ".*.$*.*..." ".........." ".664.598..455" { { 664 } { 664 598 } { 598 } }
[ length 2 = ] map-filter [ 3drop ] dip ! { { 664 598 } }
;
: part2 ( -- )
"vocab:aoc-2023/day03/input.txt" utf8 file-lines ! lines
dup [ ! lines line i
pick swap [ 1 - ?nth-of ] [ 1 + ?nth-of ] 2bi ! lines line prev next
gears [ product ] map-sum ! lines line-ratio-sum
] map-index sum nip . ! total .
;
Factor on github:
note: lemmy mangles the less-than-or-equals sign in the
possible?
function
USING: arrays assocs io.encodings.utf8 io.files kernel math
math.parser math.vectors prettyprint regexp sequences splitting
;
IN: aoc-2023.day02
: known-color ( color-phrases regexp -- n )
all-matching-subseqs [ 0 ] [
[ split-words first string>number ] map supremum
] if-empty
;
: line>known-rgb ( str -- game-id known-rgb )
": " split1 [ split-words last string>number ] dip
R/ \d+ red/ R/ \d+ green/ R/ \d+ blue/
[ known-color ] tri-curry@ tri 3array
;
: possible? ( known-rgb test-rgb -- ? )
v<= [ ] all?
;
: part1 ( -- )
"input.txt" utf8 file-lines
[ line>known-rgb 2array ] map
[ last { 12 13 14 } possible? ] filter
[ first ] map-sum .
;
: part2 ( -- )
"input.txt" utf8 file-lines
[ line>known-rgb nip product ] map-sum .
;
I would have liked to see a nice clear example at the top to help me decide if I really want to read about the language.
EDIT: First sample:
day := 1;
import "advent-prelude.noul";
puzzle_input := advent_input();
submit! 1, puzzle_input split "\n\n" map ints map sum then max;
submit! 2, puzzle_input split "\n\n" map ints map sum then sort then (_[-3:]) then sum;
Looks not too different from what you might do in Factor:
: totals ( -- seq )
puzzle-input "\n\n" split-subseq [ split-lines [ string>number ] map-sum ] map
;
: part1 ( -- ) totals supremum . ;
: part2 ( -- ) totals sort 3 tail* sum . ;
I mentioned it in a reply but it deserves its own top-level answer.
Factor, the concatenative programming language, NestedText, the configuration format, and Nu Shell, even though I don’t use it.
All of these languages are relatively succinct, and I rely on that to reduce visual and mental clutter, because I have a pea brain.
Factor, Nim, Roc, and Zsh each offer, to differing extents, some argument-then-function ordering in the syntax, which strikes me as elegant and fun, and maybe even wise. In that order, Factor does this the most (using postfix/reverse-polish-notation and managing a data stack), and Zsh the least (piping output from one command as the input for the next command).
Roc is a functional language, and an offshoot of Elm in spirit. The lead developer and community are great. Relative to Elm, it’s more inclusive and experimental in the development process, and does not primarily or exclusively target web stuff. They aim to create an ambitiously integrated development environment, especially taking advantage of any guarantees the functional design can offer.
Here’s a sample, using the |>
operator a lot, which lets you order the first argument to a function before the function IIRC:
getData = \filepath ->
filepath
|> Path.fromStr
|> File.readUtf8
|> Task.attempt \result ->
result
|> Result.withDefault ""
|> Task.succeed
Nim is so darn flexible and concise, has great compilation targets, and employs Uniform Function Call Syntax to more implicitly enable the kind of ordering in the Roc example. And you can often leave out parentheses entirely.
Factor is a full-on postfix and concatenative language, tons of fun, and turns my brain inside out a bit. I made a community for concatenative languages here on programming.dev, and while there’s little activity so far, I’ve filled the sidebar with a bunch of great resources, including links to active chats.
EDIT: !concatenative@programming.dev
The Factor REPL (“listener”) provides excellent and speedy documentation and definitions, and a step-through debugger.
One idea that seems absurd at first is that for the most part, you don’t name data variables (though you can, and you do name function parameters). It’s all about whatever’s on the top of the stack.
In some languages it’s awkward to approximate multiple return values, but in a stack-oriented language it’s natural.
In Factor, everything is space-separated, so functions (“words”) can and do include or consist of symbols. [
is not semantically something between brackets, it’s just a function that happens to be named ][
. It pops 1 item off the top of the stack (an integer), and pushes a range from 1 to that integer on to the top of the stack. ]
Here it is in my solution to the code.golf flavor of Fizz Buzz:
USING: io kernel math.functions math.parser ranges sequences ;
100 [1..b] [
dup [ 3 divisor? ] [ 5 divisor? ] bi 2dup or [
[ drop ] 2dip
[ "Fizz" "" ? ] [ "Buzz" "" ? ] bi* append
] [ 2drop number>string ] if
print
] each
And in image form for glorious syntax highlighting:
Anything between spaced brackets is a “quotation” (lambda/anonymous function).
So:
each
at the bottom.each
consumes the range and the quotation.
For each element of the range, it pushes the element then calls the quotation.dup
pushes a copy of the stack’s top item.
Say we’re in the ninth iteration of the each
loop, we’ve got 9 9
on the stack.9 9 [...] [...]
),
then bi
applies them each in turn to the single stack item directly beneath,
leaving us with 9 t f
(true, it’s divisble by three, false, it’s not by 5).2dup
copies the top two, so: 9 t f t f
or
combines the last two booleans: 9 t f t
if
(9 t f t [...] [...] if
),
which pops that final t
and calls only the first quotation.[ drop ] 2dip
takes us from 9 t f
to t f
–
it dips under the top two, drops the temporary new top, then restores the original top two.?
is like a ternary.
That first quotation will push "Fizz"
if called with t
(true) on the stack, ""
otherwise.bi*
applies the last two items (quotations) to the two values before them,
each only taking one value.
The Fizz one applies to t
and the Buzz to f
,
taking us from t f [...] [...]
to "Fizz" ""
append
joins those strings, as it would any sequence: "Fizz"
print
the string, leaving us with an empty stack,
ready for the next iteration.EDIT: Walkthrough in image form, more granular:
The ones I can get things done with:
My current obsession:
Honorable mentions:
I meant to communicate that the Redirector addon uses the given pattern to see if the entire URL string matches, not part of it. So the malicious URL does not match.
I’m wondering if there’s a real URL for which the Redirector approach will not work.