-
Notifications
You must be signed in to change notification settings - Fork 415
A short introduction to Red for Python programmers
- Words
- Evaluation order
- Datatypes
-
Blocks and series
- Creating blocks
- Accessing block elements
- Traversing a series
- Finding values in series
- Getting several values from a series at once
- Adding element to a series
- Removing items from a series
- Taking elements from series
- Changing values in series
- Moving values within series
- Sorting series
- Series as sets
- String-specific functions
- Code as data
- Conditionals
- Loops
- Collect/Keep
- Functions
- Parse
- View
- Draw
Red is a homoiconic language (Red is its own meta-language and own data-format) and an important paradigm it uses is “code is data”. A Red program is a sequence of Red values - everything is data before it’s evaluated. This makes one able to express any task using syntax that best suits it. The execution of a Red program is done by evaluating each of its constituent values in turn, according to the evaluation rules.
A special category of values is word. A word! is a symbolic value that can be used like a variable (the !
at the end denotes a datatype). Red does not have identifiers nor keywords. Words do not store values, they point to values in some context – the global context by default.
Words are formed by one or more characters from the entire Unicode range, including punctuation characters from the ASCII subset: ! & ' * + - . < = > ? _ | ~
Here are some valid words:
foo foo+bar Fahrenheit451 C° __ (two underscores)
Words can’t start with a digit and can’t contain any control characters, whitespace characters, and punctuation characters from the ASCII subset: / \ ^ , [ ] ( ) { } " # $ % @ : ;
The presence of ! & ' * + - . < = > ? _ | ~`
in words leads to the implication that they must be delimited by spaces, tabs or \[]\(){}. In Python you can write:
a+b 1and a>b
In Red you always need to put spaces between words:
a + b 1 and a > b
Please note that Red is case insensitive.
At a semantic level, the Red program consist of expressions and not values. An expression groups one or more values, and may be formed in three ways: as an application of a (prefix) function, as an infix expression which uses an operator, or as a binding of a word to refer to a value.
Functions in Red always have a fixed number of arguments (fixed arity), as opposed to Python, where one can have default arguments and variable-length arguments. Functions are called by their name followed by the arguments – no need of parentheses nor commas.
print "Hello, world!” add 2 3 find "Red" #"e"
Operators are always binary operations, like +
(addition), -
(subtraction) and so on.
Evaluation of the operands of operators has precedence over function application and binding. There is no precedence between any two operators. This is different from Python, where the operators have different precedence
2 + 2 ; evaluates to 4 2 + 3 * 4 ; evaluates to 20, not 14! max 3 + 4 5 ; evaluates to 7
As you may have guessed, ;
starts a comment until the end of the line.
Let’s take for example the following expression:
square-root 4 + 5
The operator +
has precedence over the function square-root
and that’s why Red first adds 5 to 4 and only then finds the square root of 9, resulting in 3.0.
Since the function arguments aren’t enclosed in parentheses, a programmer must know the arity of the functions.
Evaluation order can be changed by the use of parentheses:
2 + (3 * 4) ; evaluates to 14 (length? "abcd") / 2
If we had written length? "abcd" / 2
, it would have resulted in an error, because Red would first try to divide "abcd” by 2.
Red has a rich set of datatypes. Here are some types to start with:
64-bit positive or negative number that contains a decimal point.
+123.4, -123.4, 0042.0, 60'000'12'3.4
Sequence of Unicode code points (char! values) wrapped in quotes.
"Red"
Unlike Python, strings in Red are mutable. For example, compare this Python code
>>> txt = "abcd" >>> txt.upper() 'ABCD' >>> txt 'abcd'
with Red:
>> txt: "abcd" == "abcd" >> uppercase txt == "ABCD" >> txt == "ABCD"
Multiline strings are enclosed in {} and can contain double-quotes:
{This text is
split in "two" lines}
Collections of data or code that can be evaluated at any point in time. Values and expressions in a block are not evaluated by default. This is one of the most versatile Red types.
[], [one 2 "three"], [print 1.23], [x + y], [dbl: func[x][2 * x]]
Immediately evaluated block!. Evaluation can be suppressed by using quote
before a paren value. Unquoted paren values will return the type of the last expression.
(1 2 3), (3 * 4), (x + 5)
Please note that if x
doesn’t have a value in the current context, the last example will throw an error.
>> (1 + 10) == 11 >> type? (1 + 10) == integer! >> quote (1 + 10) == (1 + 10) >> type? quote (1 + 10) == paren!
Series of values delimited by slashes /. Limited in the types of values that they can contain – integers, words or parens.
buffer/1, a/b/c, data/(base + offs)
Path notation is used for indexing a block. Please note that Red uses 1-based indexing. The following Python code
>>> mylist = [3,1,4,2] >>> mylist[0] 3
Can be written in Red as follows:
>> mylist: [3 1 4 2] == [3 1 4 2] >> mylist/1 == 3
One can access the nested values in a block using as many levels of /
as needed:
>> a: [1 [2 3] "456"] == [1 [2 3] "456"] >> a/1 == 1 >> a/2 == [2 3] >> a/2/2 == 3 >> a/3/1 == #"4"
Associative array of key/value pairs (similar to Python’s dictionary)
#( ), #(a: 1 b: "two")
The keys can be any type of the following typesets: scalar!, all-word!, any-string!
Named or unnamed contexts that contain word: value
pairs.
xy: make object! [ x: 45 y: 12 mult: func[k][x + y * k] ]
Please note that at this time it is not possible to extend an object with new word: value
pairs.
The objects in Red are prototype-based, and not class-based.
You can create a new object xyz
using xy
as a prototype and describe just the new pairs:
>> xyz: make xy [z: 1000] == make object! [ x: 45 y: 12 mult: func [k][x + y * k] z: 1000 ]
You can find more about Red’s aggregate values here
user-defined functions. Functions have specification (a block that contains the arguments that the function needs for its workings and other optional information) and body (a block containing expressions):
x+y: function [x y][x + y]
There are also other kinds of functions - func, does, has - that will be explained in more details in a section dedicated to functions.
Refinement! values are symbolic values that are used as modifiers to functions or as extensions to objects, files, urls, or paths, but they are also valid standalone values.
option: /reverse
>> replace/all "Mississippi" #"i" #"e" == "Messesseppe"
Without the /all
refinement only the first "i" would be changed to "e".
Two-dimensional coordinates (two integers separated by a x
)
1x2, -5x0, -3x-25
The pair fields can be accessed by /x and /y refinments (or /1 and /2)
+, -, *, /, %, //, add, subtract, multiply, divide, remainder, and mod
can be used with pair! values.
Calendar dates, relying on the Gregorian calendar.
28-03-2021, 28/Mar/2021, 28-March-2021, 2021-03-28
As you can see, different input formats for literal dates are accepted.
The fields of any date!
value can be accessed using path accessors - /date
, /year
, /month
, day
(or alternatively just /1
/2
/3
/4
)
One can use addition and subtraction operations with date!, as well as with date! and integer!. Dates will be explored in a special section.
Three to twelve positive integers in the range from 0 to 255 separated by decimal points. Used for representing RGB and RGBA color values, ip addresses, and version numbers.
255.255.255.0
A bitset!
is an array of bits used to store boolean values. Bitset indexing is zero based with 1 values representing true, and 0 values representing false.Bitsets are used to model sets of non-negative integers such as Unicode Code Points.
make bitset! #"A"
make bitset! "abc"
>> make bitset! #"A" ; create a bitset with bit 65 set == make bitset! #{000000000000000040}
A block is a set of values arranged in some order. They can represent collections of data or code that can be evaluated upon request. Blocks are a type of series! with no restriction on the type of values that can be referenced. A block, a string, a list, a URL, a path, an email, a file, a tag, a binary, a bitset, a port, a hash, an issue, and an image are all series and can be accessed and processed in the same way with the same small set of series functions
Blocks in Red are similar to Python’s lists, but don’t forget that blocks are not evaluated by default. Compare these code snippets:
Python
>>> p_list=[2+3,5] >>> p_list [5, 5]
Red
>> red-block: [2 + 3 5] == [2 + 3 5]
As you can see, red-block remains unchanged, while p_list is formed by the evaluated values of its constituents.
Blocks are created by enclosing values (separated by whitespaces) in square brackets [ ]
[1 2 3] [42 6 * 7 "forty-two" forty two]
Except literally, blocks can be created at runtime using a make
constructor:
>> make block! 20 == []
The above code creates and empty block pre-allocated for 20 elements.
Block can also be created by converting other values:
>> msg: "send %reference.pdf to [email protected] at 11:00" == "send %reference.pdf to [email protected] at 11:00" >> type? msg == string! >> to block! msg == [send %reference.pdf to [email protected] at 11:00:00]`
Here msg
is of string! type. When converted to a block!
, each part of the string is converted to a Red value (of course if it represents a valid Red value):
>> foreach value to block! msg[print [value ":" type? value]] send : word reference.pdf : file to : word [email protected] : email at : word 11:00:00 : time
The above code iterates over the items of the block created from a string using to
conversion and prints the value and its type.
Please note that to
function (technically it’s an action!
) expects a datatype OR an example value to which to convert the given value. This means that instead of block!
we can use any literal block, even`[]`:
>> to [] msg == [send %reference.pdf to [email protected] at 11:00:00]
Now that you know what a block is and how you create one, let’s try to access block’s items. Let’s work with
data: ["Red" "Yellow" "Green" "Cyan" "Blue" "Magenta"]
The simplest way one can reference an item in a block is using the item’s index in the block. Unlike Python, Red uses 1-based indexing. So, to get the first item we use path notation
and an integer index:
>> data/1 == "Red" >> data/2 == "Yellow"
Alternatively, we can use pick
:
>> pick data 3 == "Green"
Please note that in Red it’s not possible to use path notation
to index a literal block (or series). It’s perfectly valid to write in Python:
>>> [2,3,1][2] 1
To achieve a similar behavior in red we use pick
:
>> pick [2 3 1] 3 == 1
A useful feature of pick
is the possibility to use a logic!
value for the index. The true
value refers to the first item in the block (series) and the false
value – to the second item.
>> pick data 3 < 5 == "Red" >> pick data 2 > 3 == "Yellow"
Speaking of first and second items of a block, Red has predefined functions for accessing the first 5 items of a series:
>> first data == "Red" >> second data == "Yellow" >> third data == "Green" >> fourth data == "Cyan" >> fifth data == "Blue"
Let’s consider another block of values: ` signal: [a 2 7 b 1 8 c 2 8] . Here `a b c
are just `word!`s – that is they represent themselves until they have some value in some context.
>> first signal == a
So , the first item if signal
is just a
.
>> type? first signal == word!
If we try to get the value a
refers to, we get an error:
>> get first signal *** Script Error: a has no value *** Where: get *** Stack:
However, if we assign a
value in the current (global) context, the first item of signal
will be referring to it:
>> a: "abc" == "abc" >> get first signal == "abc"
Of what use are the words in a block? We can use them to mark positions in the block for an easy access:
== 7 >> signal/a == 2 >> signal/b == 1 >> signal/c == 2
Alternatively, we can use select
to find a value in a series and get the value after it:
>> select signal 'a == 2 >> select signal 2 == 7 >>
Let’s try to navigate within a block/series. Our new block will be b: [1 2.0 #"3" "four"]
head
returns a series at its first index. Please note – the entire series, not the element at that position.
>> b == [1 2.0 #"3" "four"] >> head b == [1 2.0 #"3" "four"]
Similarly, there is tail
that returns a series at the index after its last value.
>> tail b == []
Here []
is an empty block – there are no elements in the series at its tail.
If we are interested in the elements of a series between its head and tail, we can use next
to iterate over the series. next
returns a series at the next index:
>> next b == [2.0 #"3" "four"] >>
Please be careful - next
doesn’t update the series, that’s why you need to use a set-word!
to re-assign it:
>> next b == [2.0 #"3" "four"] >> b == [1 2.0 #"3" "four"] >> b: next b == [2.0 #"3" "four"] >> b == [2.0 #"3" "four"]
Let’s compare Red’s next
to Python’s next()
method.
>>> a = [1,'2',[1,2,3]] >>> a_it = iter(a) >>> next(a_it) 1 >>> next(a_it) '2' >>> next(a_it) [1, 2, 3]
Python’s next()` returns a single element and not the list. If at any point you convert the iterator to a list using list(a_it)
or [*a_it]
, the iterator is exhausted and a subsequent call to next(a_it)
raises a StopIteration
exception.
We said that head
refers to the series at its first index – index 1. We can check the current index of a series with index?
>> b == [2.0 #"3" "four"] >> index? b == 2 >> head b == [1 2.0 #"3" "four"] >> index? head b == 1 >> index? tail b == 5
Don’t forget that tail
returns the series at the index after its last item. So index? tail b
returns one more than the length of b
.
We can find the length of a series using length?
:
>> length? b == 4
We can check if a series is at its head (first index) or tail with head?
and tail?
respectively:
>> b == [1 2.0 #"3" "four"] >> head? b == true >> b: next b == [2.0 #"3" "four"] >> head? b == false >> b: tail b == [] >> tail? b == true
We saw that we can go from head to tail in a series using next
. Similarly, we can go backwards with back
:
>> b == [1 2.0 #"3" "four"] >> tail b == [] >> back tail b == ["four"]
Both next
and back
address the relative index of a series one position at a time. In contrast, skip
allows bigger “jumps” relative to the current index.
>> head? b == true >> skip b 2 == [#"3" "four"]
The series is at its head (first index) and we are `skip`ping 2 indices. The result is the series 2 indices after its head:
>> index? skip b 2 == 3
Don’t forget that the series head has index 1. We can use negative offset as a second argument to skip
:
>> skip tail b -2 == [#"3" "four"]
We start at the tail of b
and go two steps backwards, we will get the series two indices before its tail.
>> index? tail b == 5 >> index? skip tail b -2 == 3
Please note that skip
, next
and back
don’t go beyond series’ head/tail:
>> index? skip b 20 == 5 >> index? skip tail b -20 == 1 >>
The at
functions has functionality similar to skip
, but returns the series at a given index, instead of at an offset (relative to the current index).
>> head? b == true >> skip b 1 == [2.0 #"3" "four"] >> at b 1 == [1 2.0 #"3" "four"]
at
allows a negative integer for its index
argument:
>> at tail b -1 == ["four"]
We will finish our tour of series navigation functions with offset?
. Not surprisingly, It returns the offset between two series positions.
>> offset? b tail b == 4 >> b == [1 2.0 #"3" "four"] >> subtract index? tail b index? b == 4
As you can see, offset?
is the difference between two indices in a series.
To search a series!
(or a bitset!
,typeset!
, or a map!
) for a value, use find
:
>> find "Red is a full-stack programming language" "full" == "full-stack programming language" >> find [a: 345.5 b: 56] integer! == [56] >> find "Red" charset "aoeiu" == "ed"
find
returns the series where a value is found, or NONE.
It has several refinements:
/part
limits the length of the search, a length
argument must be supplied.
/only
treats a series search value as a single value. Please note the difference:
>> find [1 2 3 4 [2 3] 5] [2 3] == [2 3 4 [2 3] 5] >> find/only [1 2 3 4 [2 3] 5] [2 3] == [[2 3] 5]
` /case` perform a case-sensitive search.
/skip
treat the series as fixed size records, a size
argument is expected.
>> find "abcdef" #"d" == "def" >> find/skip "abcdef" #"d" 2 == none
In the second example the input is treted as a series of length 2 records (you can think of it as ["ab" "cd" "ef"]) and #”d” is none of them.
/last
finds the last occurrence of value, from the tail:
>> find/last "Mississippi" "ssi" == "ssippi"
/reverse
finds the last occurrence of value, from the current index:
>> find/reverse find/last "Mississippi" "ssi" "s" == "sissippi"
/tail
returns the tail of the match found, rather than the head.
>> find/tail %report.pdf #"." == %pdf
/match
match at current index only and return tail of match:
>> find "abracadabra" "abra" == "abracadabra" >> find next "abracadabra" "abra" == "abra" >> find/match "abracadabra" "abra" == "cadabra" >> find/match next "abracadabra" "abra" == none
We saw how one can access a single value from a series using index and path notation, pick
and select
. It is very often necessary to get more than one value from a series at once. In such cases we use copy
.
>> c: copy b == [1 2.0 #"3" "four"]
Here we created a new series c
with values that are copies of the values of b
. If we just used a set-word!
without the copy
function, we would have created a reference to b
. In such case any change in either b
or c
would result in changing the other, as they share a single series:
>> b == [1 2.0 #"3" "four"] >> c: b == [1 2.0 #"3" "four"] >> b/1: 11 == 11 >> b == [11 2.0 #"3" "four"] >> c == [11 2.0 #"3" "four"]
When the series contains nested values, they are normally copied as a reference:
>> colors: [warm: ["red" "yellow" "orange"] cool: ["blue" "green" "purple"]] == [warm: ["red" "yellow" "orange"] cool: ["blue" "green" "purple"]] >> colors-copy: copy colors == [warm: ["red" "yellow" "orange"] cool: ["blue" "green" "purple"]] >> colors/warm/1: "papaya" == "papaya" >> colors == [warm: ["papaya" "yellow" "orange"] cool: ["blue" "green" "purple"]] >> colors-copy == [warm: ["papaya" "yellow" "orange"] cool: ["blue" "green" "purple"]]
As you see, the change in the original series leads to a change in the copy. In order to avoid this, you need to use copy/deep
- it copies the nested values too:
>> colors-deep-copy: copy/deep colors == [warm: ["papaya" "yellow" "orange"] cool: ["blue" "green" "purple"]] >> colors/warm/1: "red" == "red" >> colors == [warm: ["red" "yellow" "orange"] cool: ["blue" "green" "purple"]] >> colors-deep-copy == [warm: ["papaya" "yellow" "orange"] cool: ["blue" "green" "purple"]]
Red’s copy/deep
is similar to Python’s copy.deepcopy()
If want to copy just a part of the series, we can use copy
with refinement /part
. The first argument indicates where to start, the second – how many elements to copy.
>> b: [1 2.0 #"3" "four"] == [1 2.0 #"3" "four"] >> copy/part b 2 == [1 2.0] >> copy/part at b 2 2 == [2.0 #"3"] >> copy/part tail b -3 == [2.0 #"3" "four"] >>
In the second example we start not at the head of the series, but at its second index.
You can think of copy/part
as using Python slices:
>>> a=[1,2.0,'3','four'] >>> a[:2] [1, 2.0] >>> a[-3:] [2.0, '3', 'four']
You might be now wondering if it’s possible to mimic Pythons slicing with a step in Red. Python does it using the third parameter of the slice notation.
a[::2] [1, '3']
Red uses a different function for this - extract
:
>> extract b 2 == [1 #"3"] >> extract next b 2 == [2.0 "four"]
Until now we were only copying elements from a series. Let’s see how to add new items. If we need to add one or more elements at the tail of a series, we do it with append
:
>> append b 5 == [1 2.0 #"3" "four" 5]
We can append several copies of the element using /dup
refinement:
>> append/dup b 6 3 == [1 2.0 #"3" "four" 5 6 6 6]
Python has two separate methods for adding new elements to a list as a single value or multiple values - append()
and `extend()
>>> a=[1,2,3,4] >>> a.append(5) >>> a [1, 2, 3, 4, 5] >>> a.append([6,7]) >>> a [1, 2, 3, 4, 5, [6, 7]] >>> a.extend([8,9]) >>> a [1, 2, 3, 4, 5, [6, 7], 8, 9]
Red uses the /only
refinement to append the new value as block:
>> a: [1 2 3 4] == [1 2 3 4] >> append a [5 6] == [1 2 3 4 5 6] >> append/only a [7 8] == [1 2 3 4 5 6 [7 8]]
We can add elements at any position in a series using insert
>> b: [1 2.0 #"3" "four" 5 6 6 6] == [1 2.0 #"3" "four" 5 6 6 6] >> insert b 'zero == [1 2.0 #"3" "four" 5 6 6 6] >> b == [zero 1 2.0 #"3" "four" 5 6 6 6] >> insert/only at b 2 [2] == [1 2.0 #"3" "four" 5 6 6 6] >> b == [zero [2] 1 2.0 #"3" "four" 5 6 6 6]
Please note that we need to use the only
refinement when we need the new element be added as a block, otherwise the block contents would be added.
We can remove values from a series using remove
:
>> s: "Hello world!" == "Hello world!" >> remove s == "ello world!" >> s == "ello world!" >>
remove
returns the series at the same index after removing
In Python you use del
to remove an item at the specified index (I’ll mention pop()
in a subsequent section):
>>> a=[3,1,4,1,5] >>> del a[2] >>> a [3, 1, 1, 5]
The argument can be a series at some specific index:
s: "Hello world!" == "Hello world!" >> remove at s 6 == "world!" >> s == "Helloworld!"
If we need to remove more than one value, we can use the /part
refinement:
>> remove/part at s 6 3 == "ld!" >> s == "Hellold!" >>
One way to do this in Python is to use del
with list slicing, like del a[2:5]
Sometimes the whole series should the emptied, or all elements after certain index to be removed. It can be done with remove/part
, but there is a special function for this - clear
. It removes series values from current index to tail and returns the new tail.
>> s: "Hello world!" == "Hello world!" >> clear at s 6 == "" >> s == "Hello"
A special case of removing items from series is getting rid of whitespaces from srtings or none
from blocks. Red has a special function for this operation - trim
:
>> txt: " Removes space from a string or NONE from a block. " == { Removes space from a string or NONE from a block. } >> trim txt == "Removes space from a string or NONE from a block." >> trim/all txt == "RemovesspacefromastringorNONEfromablock." >> trim/with txt #"e" == "RmovsspacfromastringorNONEfromablock." >> data == [345 none 1123 none none 0 -34] >> trim reduce data == [345 1123 0 -34]
We saw that we could remove elements from series. Sometimes we need to use these elements and not just discard them. This is done using take
:
>> a == ["yellow" "green" "blue"] >> color: take a == "yellow" >> color == "yellow" >> a == ["green" "blue"]
The element at the current index was removed from the series, and returned as result. /part
refinement is available in take’ too. Use `/last
when you need to take element(s) from the tail of a series.
Python’s pop()
is similar to Red’s take
(with no /part
refinement)
>>> a=[3,1,4,1,5] >>> last_a=a.pop() >>> a [3, 1, 4, 1] >>> last_a 5
>> a: [3 1 4 1 5] == [3 1 4 1 5] >> last-a: take/last a == 5 >> a == [3 1 4 1]
The easiest way to change a value in a series in Red is to use path notation and specify the index of the value we want to change:
>> a: [3 1 4 1 5] == [3 1 4 1 5] >> a/2: 10 == 10 >> a == [3 10 4 1 5]
This corresponds to Python’s assignment that refers to the item’s index within a list:
>>> a=[3,1,4,1,5] >>> a[1]=10 >>> a [3, 10, 4, 1, 5]
To change a value (or consecutive values) in Red wecan also use change
. We need to indicate the series we want to change and the new value. If we give a single value, the value at the current index of the series will be changed to the new value:
>> a: [3 1 4 1 5] == [3 1 4 1 5] >> change at a 2 10 == [4 1 5] >> a == [3 10 4 1 5] >>
If the new value is a block, Red will change the values starting at the current index with the values from the block, appending the new values if needed:
>> b: [2 3 1] == [2 3 1] >> change at b 2 [4 5 6 7] == [] >> b == [2 4 5 6 7]
In contrast, Python changes a single value with a single value, keeping the list:
>>> b=[2,3,1] >>> b[1]=[4,5,6,7] >>> b [2, [4, 5, 6, 7], 1]
If we need to do a similar thing in Red, we would use the /only
refinement (please note how the similar actions are described with the same word - only
in this case, analogous to /only
in append
and insert
)
>> b: [2 3 1] == [2 3 1] >> change/only at b 2 [4 5 6 7] == [1] >> b == [2 [4 5 6 7] 1]
If we need to change a given number of values with several values, we can do it with the /part
refinement:
>> b: [2 3 1] == [2 3 1] >> change/part at b 2 [4 5 6 7] 1 == [1] >> b == [2 4 5 6 7 1]
While change
changes a series based on index, replace
changes the series based on value/pattern.
replace
series pattern value – replaces a pattern (a specific value or a parse
rule) in a series (any-block!, aby-string!, binary! or vector!) with a new value, in place.
>> fruit: "Äpfel" == "Äpfel" >> replace fruit #"Ä" "Ae" == "Aepfel" >> data: [pos: 10x10 speed: 3x2 mass: 20 grid: 10x10] == [pos: 10x10 speed: 3x2 mass: 20 grid: 10x10] >> replace/all data 10x10 5x5 == [pos: 5x5 speed: 3x2 mass: 20 grid: 5x5]
As you see, we can replace pairs in blocks as easiliy as characters in strings.
Although parse
needs a separate tutorial, lets see how replace
can benefit from using a parse
rule for its pattern:
>> replace/all data pair! 0x0 == [pos: 0x0 speed: 0x0 mass: 20 grid: 0x0]
I used the last value of ` data` block from the previous example and the simple pair!
rule with replace/all
- it replaced all values of pair!
datatype in the block with a new value – 0x0
.
Every series is an ordered collection of elements. Sometimes we need to change the order of the elements in a block/series. In such cases, we use move
:
>> a: ["red" "green" "blue" "yellow"] == ["red" "green" "blue" "yellow"] >> move back tail a next a == ["blue"] >> a == ["red" "yellow" "green" "blue"]
The two arguments to move
are just series – that’s why we can move elements from one series to another, not just from one position in a series to another position in the same series:
>> b: ["cyan" "magenta"] == ["cyan" "magenta"] >> move at a 2 b == ["green" "blue"] >> b == ["yellow" "cyan" "magenta"]
move
has a /part
refinement too for moving more than one element at once.
When we need to exchange a single element between series, we use swap
:
>> a == ["red" "green" "blue"] >> b == ["yellow" "cyan" "magenta"] >> swap a b == ["yellow" "green" "blue"] >> a == ["yellow" "green" "blue"] >> b == ["red" "cyan" "magenta"]
A special case of moving values within series is sorting. The goal of sorting is to arrange the elements of a series according some criterion, for example a number list from smallest number to the largest.
>> a: [53 81 67 51 13 4 3 71 48 92] == [53 81 67 51 13 4 3 71 48 92] >> sort copy a == [3 4 13 48 51 53 67 71 81 92] >> a == [53 81 67 51 13 4 3 71 48 92]
When used without refinemens, sort
arranges the items in ascending order, as it’s seen from the example above. sort
modifies the series, that’s why you need to make a copy of your data if you still need the original arrangement. The Python analogues are as follows:
-
Sorting in Python and Red
Python | Red |
---|---|
list.sort() |
sort list |
sorted(list) |
sort copy list |
When you need to sort in descending order, use the /reverse
refinement:
>> days: ["Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday"] == ["Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" ... >> probe sort/reverse days ["Wednesday" "Tuesday" "Thursday" "Sunday" "Saturday" "Monday" "Friday"] == ["Wednesday" "Tuesday" "Thursday" "Sunday" "Saturday" "Monday" "Friday"]
You can sort just the initial part of a series using the /part
refinement:
>> text: ["Lorem" "ipsum" "dolor" "sit" "amet," "consectetur" "adipiscing" "elit."] == ["Lorem" "ipsum" "dolor" "sit" "amet," "consectetur" "adipiscing" "elit."] >> sort/part text 5 == ["amet," "dolor" "ipsum" "Lorem" "sit" "consectetur" "adipiscing" "elit."]
You can see that only the first five words have been sorted and the remaining block stayed unsorted.
An interesting feature of sort
is that it can treat the series as a set of fixed size records. Let’s illustrate this concept with the following example. Let’s assume we have the following map
>> id-name-map: #[3 "John" 5 "Johan" 1 "Ivan" 2 "Jean" 4 "Giovanni" 6 "Juan"] == #[ 3 "John" 5 "Johan" 1 "Ivan" 2 "Jean" 4 "Giovanni" 6 "Juan" ] >> id-name-map/3 == "John" >> id-name-block: to block! id-name-map == [ 3 "John" 5 "Johan" 1 "Ivan" 2 "Jean" 4 "Giovanni" 6 "Juan... >> id-name-block/3 == 5 >> sort/skip id-name-block 2 == [ 1 "Ivan" 2 "Jean" 3 "John" 4 "Giovanni" 5 "Johan" 6 "Juan...
id-name-map
is a map
that associates an id to a name (note that it’s not guaranteed that the key-value pairs are in any specific order in a map; sort
doesn’t work on maps). We convert the map to a block. The block id-name-block
is flat and id – name pairs are preserved. We sort the block using the /skip
refinement with value 2 – that is sort
treates the block as a set of records with size 2 by their first firld. It sorts the id
s and the names “associated” with them.
When we treat a series as fixed size records, we can also use /all
- it compares all fields.
It is possible to use /compare
refinement. It accepts a number (offset) or a function. When the argument to /compare
is an offset, we also need to use the /skip
refinement, because it supposes we treat the series as fixed size records. It uses the offset to sort the records by their n
th field, where n
is the argument to /compare
.
>> shapes: [ [ triangle 50 255 [ rectangle 225 340 [ square 200 200 [ ] == [ triangle 50 255 rectangle 225 340 square 200 200 ] >> sort/skip/compare copy shapes 3 1 == [ rectangle 225 340 square 200 200 triangle 50 255 ] >> sort/skip/compare copy shapes 3 2 == [ triangle 50 255 square 200 200 rectangle 225 340 ] >> sort/skip/compare copy shapes 3 3 == [ square 200 200 triangle 50 255 rectangle 225 340 ]
We have a block of 9 values, which we want to treat as records of size 3 – that is the name of the shape, it’s x
coordinate and it’s y
coordinate. ` sort/skip/compare copy shapes 3 1` sorts the block as records of size 3 (/skip
and parameter 3) by the 1st value of each record (/compare
with argument 1). The next examples demonstrate sorting according to the 2nd (x
coordinate) and 3rd (y
coordinate) fields.
When the argument to the /compare
refinement is a function, it needs to be a function with exactly 2 arguments, because it will be called for each two elements that are currently sorted. We’ll talk about functions in more details in a dedicated section. For the moment let’s just sort a block of strings according to their length:
colors: ["transparent" "gray" "red" "white" "beige" "aqua" "black" "blue"] sort/compare colors func[x y][(length? x) < length? y] == ["red" "gray" "blue" "aqua" "black" "white" "beige" "transparent"]
I’ve used an anonymous function with two arguments x
and y
, that compares wherher the length of the first argument is less than the length of the second. sort
used this function an argument for the /compare
refinement and sorted the strings according the comparison in the function.
Sometimes we only need to know what the series elements are, regardless of their count and order. In such cases we treat the series as a set.
We re move the duplicates in a series using unique
:
>> a: [3 1 4 1 5] == [3 1 4 1 5] >> unique a == [3 1 4 5] >> a == [3 1 4 1 5] >> unique "AbracadABra" == "Abrcd"
Please note that in the last example Red has removed the lowercase a
to. By default, Red is case insensitive. In order to distinguish between uppercase ans lowercase characters, we need to use the case
refinement:
>> unique/case "AbracadABra" == "AbracdB"
The series is not updated by the call to unique
- you need to reassign it if you want to use the result as a new value for the series.
Please note that there is no set
datatype in Red as in Python:
>>> a=[3,1,4,1,5] >>> set_a=set(a) >>> set_a {1, 3, 4, 5} >>> type(set_a) <class 'set'>
Red provides the following operations on data sets: union
, difference
, intersect
and exclude
.
Let’s take a look at some functions that works only in string series.
split
breaks a string into pieces using the specified delimiter(s). The delimiter can be a character, a string, or a bitset.
>> legend: "Break a string series into pieces using the provided delimiters" == {Break a string series into pieces using the provided delimiters} >> split legend space == ["Break" "a" "string" "series" "into" "pieces" "using" "the" "provided" "delimiters"]
space
is a predefined value for the space character #" "
. The result of split
is a block of strings. Spliting on string values is straightforward:
>> split "Mississippi" "ss" == ["Mi" "i" "ippi"]
A bitset!
is an array of bits that is used to store boolean values. Bitset indexing is zero based with 1 values representing true
, and 0 values representing false
. Bitsets are used to model sets of non-negative integers such as Unicode Code Points.
>> make bitset! #"A" ; create a bitset with bit 65 set == make bitset! #{000000000000000040} >> make bitset! "hi" ; create a bitset with bits 104 and 105 set == make bitset! #{00000000000000000000000000C0}
Red provides a shortcut for make bitset!
- charset
. This is what we are going to use in our example of splitting on a bitset. Let’s split a string on vowels aoeiu
:
>> str: "A vowel is a syllabic speech sound pronounced..." == "A vowel is a syllabic speech sound pronounced..." >> vowel: charset "AOEIUaoeiu" == make bitset! #{000000000000000044410400444104} >> split str vowel == ["" " v" "w" "l " "s " " syll" "b" "c sp" "" "ch s" "" "nd pr" "n" "" "nc" "d..."]
pad
- as its name implies, pads a string (or a formed value) with spaces, on the right side by default.
>> pad "text" 10 == "text "
Use the /left
refinement when you need to pad the string on the left side.
>> foreach n [999 15 7 1078][print pad/left n 4] 999 15 7 1078
I will introduce the foreach
loop in a subsequent section – it goes through all the items in a series. In this example, I printed each element of the block padded with spaces to four characters on the left side. You can use another character for padding with /with
refinement.
When you need to change the case of a string, you can use lowercase
and uppercase
:
>> lowercase "Red and Python" == "red and python" >> uppercase "red and python" == "RED AND PYTHON"
Note that they change the string in place – make a copy
when you need to preserve the original formatting. Python’s upper()
and lower()
methods return a new string.
Red provides functions to encode/decode strings and binary! values to/from binary-coded strings. enbase
encodes a string into a binary-coded string; debase
decodes a binary-coded string to binary value. The possible bases are 2, 16, 58 and 64. The default is BASE-64. Use /base
refinement with when you need one of 2, 16 or 58.
>> enbase "binary-coded" == "YmluYXJ5LWNvZGVk" >> debase "YmluYXJ5LWNvZGVk" == #{62696E6172792D636F646564} >> to-string debase "YmluYXJ5LWNvZGVk" == "binary-coded"
Note that debase
returns a binary!
- that’s why you need to convert the result explicitly to a string when necessary.
>> enbase/base "15" 2 == "0011000100110101" >> enbase/base to-binary 15 2 == "00000000000000000000000000001111"
In the example above, you can see how you can convert decimal integers to binary.
You can use compress
to compress data of any-string!
or binary!
type and decompress
to decompress it. ZLIB and DEFLATE formats are supported.
As you already know, an important paradigm in Red is “code is data”. We said that everything is just data until evaluated.
Usually every computer program starts as text that is analyzed, parsed and interpreted/compiled. Red has the load
function that reads and evaluates a source and returns a value or block of values.
>> src: {n: 5 loop n [print "Hello world!"]} == {n: 5 loop n [print "Hello world!"]} >> src->code: load src == [n: 5 loop n [print "Hello world!"]] >> foreach item src->code [print[mold item ":" type? item]] n: : set-word 5 : integer loop : word n : word [print "Hello world!"] : block
src
is a string. We load it to a block named scr→code. Red has analyzed the string and converted each part to a Red-value. I used `foreach`to traverse the block and print the molded (more about molding will follow soon) value and its Red type.
load
has several refinements like /next
(loads only the next value), /part
(limits the loading to a certain position) or /as
(specifies the type of data – e.g. bmp, gif, jpeg, png)
When we want not only to load the data (convert it to Red values), but to execute it, we use do
. It evaluates and executes all the values and returns the last one.
>> src->code == [n: 5 loop n [print "Hello world!"]] >> do src->code Hello world! Hello world! Hello world! Hello world! Hello world!
Here we used do
to execute a block of Red values (src→code
). Note that we could use do
just as easily with src
, that is with a string value.
>> do src Hello world! Hello world! Hello world! Hello world! Hello world!
do
has /next
refinement too (do only the next value)
do
is similar to Python 3 exec()
function. Don’t forget that do
returns the result of the last evaluation; exec()
doesn’t return any value (returns None
). Python’s eval()
returns a value, but it only accepts a single expression.
reduce
returns a copy of a block with all its expressions evaluated. This is very useful when we need to use the data from a block that was created dynamically in another function or DSL (like View or Draw).
>> str: "some text" == "some text" >> n: 10 == 10 >> data: ['num 2 * n 'len length? str] == ['num 2 * n 'len length? str] >> reduce data == [num 20 len 9]
data
is composed of seven values with the following datatypes: lit-word, integer, word, word, lit-word ,word and word. What reduce
does is get the values words refer to, evaluate all the expressions and collect the results in a block. lit-words
evaluate to themselves, that’s why they remain in the reduced block.
>> select reduce data 'num == 20
You can use the /into
refinement of reduce
- it allows you to append the results from reduce
to the block you have given as an argument to /into
, instead of creating a new block.
compose
is similar to reduce
but it only evaluates the parens in a block.
>> set [x y size] [32 25 20] == [32 25 20] >> x == 32 >> y == 25 >> size == 20 >> compose[circle (as-pair x * size y * size) 100] == [circle 640x500 100]
x
, y
and size
are all words that have values in the current context. as-pair
is a Red function that takes two numbers and returns a pair!
composed of them. circle
is part of Draw
dialect and draws a circle (or an ellipse) with given center (a pair of coordinates) and a radius (or radii for ellipses). If I had used reduce
in the example above, I would have gotten an error * Script Error: circle has no value
- that’s why in the previous example I used lit-words
('num
and 'len
).
rejoin
reduces and joins a block of values.
>> toy: "dog" == "dog" >> qty: 10 == 10 >> toy-ref: rejoin[toy ": " qty " pieces"] == "dog: 10 pieces"
rejoin
evaluates all the values in the block and joins them to a new series. The type of the result is implied by the first value of the block:
>> rejoin [qty [20 12]] == "1020 12" >> rejoin [[20 12 ] qty] == [20 12 10] >> rejoin ["file" %.ext] == "file.ext" >> rejoin [%file ".ext"] == %file.ext
repend
appends a reduced value to a series and returns the series head:
>> toy2: "doll" == "doll" >> qty2: 12 == 12 >> repend copy toy-ref ["; " toy2 ": " qty2 " pieces"] == "dog: 10 pieces; doll: 12 pieces"
Note how repend
reduces the values before appending them, in contrast to append
:
>> values: [20 15] == [20 15] >> repend copy values [qty qty2] == [20 15 10 12] >> append copy values [qty qty2] == [20 15 qty qty2]
append
does not evaluate the values, as seen from the example above - qty
and qty2
remain just words.
repend
has a refinement /only
for appending a block of values as a block (of reduced values).
form
returns a user-friendly string representation of a value:
>> a: ["red" "orange" "yellow"] == ["red" "orange" "yellow"] >> form a == "red orange yellow"
As we said before, Red does not have any keywords. Where some other programming languages use special constructs for control flow, Red uses (native) functions.
When we want to execute some code depending on a condition, we use `if `. It expects two arguments: a conditional expression and a block to evaluate. If the conditional expression is true, the block is evaluated, otherwise the function returns none. Using blocks as code is a common pattern in Red. Blocks stay unevaluated until feeded into some function.
>> hooray!: [print "Weekend at last!"] == [print "Weekend at last!"] >> if now/weekday > 5 hooray! Weekend at last! >> now/weekday == 6
hooray!
is just a block consisting of a word! print
and a string! "Weekend at last!". Red just makes the set-word!
hooray!
to refer to the literal block that follows. At this point, print
inside the block is just a word and doesn’t mean anything.
The conditional expression in our example is now/weekday > 5
. now
is a native function that returns a value of date!
datatype, set to the current date and time. /weekday
is a refinement!
to now
that determines which day of the week is a given date, 1 for Monday. So we simply check if the weekday is greater then Friday and if it is, we want the block that we have given as a second argument evaluated. More often the then-block argument of if
is a literal block, but don’t forget that if could have been declared and even changed before its use. Or it can be changed after its first use and used for another purposes.
Red’s if condition then-block
works similar to Pythons if condition: code-block
, where code-block is either a one line of code immediately following the semicolon, or an indented block of code.
Now it’s the right time to compare the comparison operators in Python and Red:
Python | Red | Name |
---|---|---|
== |
= |
Equal to |
!= |
<> |
Not equal to |
> |
> |
Greater than |
< |
< |
Less than |
>= |
>= |
Greater than or equal to |
<= |
<= |
Less than or equal to |
Note that the simple comparison is just =
in Red. There is ==
too, but it performs a stricter comparison, taking also under account the datatypes of the values:
>> 345 = 345.0 == true >> 345 == 345.0 == false
not
is used when we need to reverse the meaning of a Boolean expression. Remember that booleans are a logic!
type in Red. True is indicated by any one of true, on, yes
; False – by any one of false, off, no
.
>> not true == false >> not off == true >> not not no == false >> not 10 > 20 == true
You can write if not condition
, but there is a convenient function for this pattern, unless
:
>> a: 10 == 10 >> unless a >= 20 [print "a is less then 20"] a is less then 20
if
and unless
execute the code in the then-block when the condition is met; they do not provide an alternative. If you need to execute an appropriate code for the either Boolean results of a condition, you use either
:
>> print either now/weekday > 5 ["Weekend"]["workday"] workday
As you can see, either
corresponds to Pythons if-else
statement – if the condition is true, the first block is executed, otherwise – the second one.
If you need to reproduce the Pythonic if-elif-else
, you’ll need to cascade two or more either
function calls: either cond1 [true-block-1][either cond2[true-block-2][false-block]]
.
Sometimes you need to select one block of code to execute from many, based on which one has a true condition.
color: "Red" RYB-type: case [ find ["Red" "Yellow" "Blue"] color ["Primary color"] find ["Orange" "Green" "Purple"] color ["Secondary color"] find ["Vermilion" "Amber" "Chartreuse" "Teal" "Violet" "Magenta"] color ["Tertiary color"] ] print RYB-type --- Primary color
The syntax is case block
, where block
is a block of pairs, each pair consisting of condition and block to execute. case
evaluates the block after the first true condition.
One important thing to remember about case
is that the pairs don’t have to be connected in any way. There’s a /all
refinement, that evaluates the block after every true condition.
color: "Teal" case/all [ true [prin [form color " is a "]] find ["Red" "Yellow" "Blue"] color [print "Primary color"] find ["Orange" "Green" "Purple"] color [print "Secondary color"] find ["Vermilion" "Amber" "Chartreuse" "Teal" "Violet" "Magenta"] color [print "Tertiary color"] true [print "Analysls complete! "] ] --- Teal is a Tertiary color Analysls complete!
Another form of branching is achieved using switch
. It accepts a value of any type as a first argument and a block, consisting of any number of sets - values followed by a block - as its second argument. The block after the value that matches the first argument is evaluated.
switch 5 [ 1 2 3 ['low] 4 5 6 ['mid] 7 8 9 ['high] ] --- "mid"
If the value is not found in the block, switch
returns none
. If you need it to return some specific value in case the first argument is not found, use /default
refinement:
color: "Orange" switch/default color [ "Blue" [0.0.255] "Red" [255.0.0] "Beige" [255.228.196] ] [0.0.0] --- 0.0.0
Python doesn’t have a switch
or case
statement yet (Python 3.10 is going to have a match/case
syntax. Until then you can use if/elif/else
statement, or use a get()
method with a dictionary as a workaround.
Very often the Boolean condition is not a simple one but compound, consisting of different parts combined using logcal operators. We already used not
, which returns the logical complement of a value.
or
returns true
if either of the arguments or both of them are true.
>> a: 10 == 10 >> a > 0 or (a < 20) == true
Please note the use of parentheses around the right hand side argument of or
. This is due to the fact that or
is an op! (infix function) and there is no precedence. The left-hand side argument is if type logic!
(a > 0
is evaluated to true
) and if there were no parentheses, Red would have tried to calculate true or a
first. But a
is a number and that would have resulted in an error.
and
returns true
only if both arguments are true
.
xor
returns true if only one of the arguments is true.
or
, and
and xor
are also used for the bitwise operations on numbers.
Sometimes there are a lot of conditons that should be combined. It is often more convenient to use all
and any
functions. Let’s change our last example so that it uses all
:
>> all [a > 0 a < 20 30] == 30
So, all
takes a block of values, evaluates them and returns the last evaluated value if all are truthy, or none
otherwise.
any
evaluates and returns the first truthy value, if any; else NONE.
In order to demonstrate any
, let’s pretend that we want to check if a given point is outside of a given rectangle. The point is defined as a pair!
of integers and the rectangle – as two pairs of integers (it’s top-left and bottom-right corners).
p: 50x23 ; point with x = 50, y = 23 tl: 40x40 ; top-left corner, x = 40, y = 40 br: 100x100 ; bottom-right corner, x = 100, y = 100 any [ p/x < tl/x p/y < tl/y p/x > br/x p/y > br/y ] --- true
The y
coordinate of our point is less than the y
coordinate of the top-left corner of the rectangle. Only this condition is true, but it is sufficient to know that the point lies outside of the rectangle.
Be carefull when you try to apply some patterns you may have adopted with Python: in Python empty strings / lists / tuples / dictionaries etc. have falsy
Boolean values, zero numeric values – too. This is not the case in Red:
>> to logic! [] == true >> to logic! "" == true >> to logic! #[] == true >> to logic! 0 == true >> to logic! 0.0 == true
On the other hand, the Boolean value of none
is false
:
>> to logic! none == false
The simplest of the looping constructs in Red is loop
. It takes a value (an integer! or a float! – that is automatically truncated to an integer) and a block as its arguments and evaluates the block as many times as the value. Of course the value can be a word and not just a literal numeric value.
>> loop 5 [print "I Am a Strange Loop"] I Am a Strange Loop I Am a Strange Loop I Am a Strange Loop I Am a Strange Loop I Am a Strange Loop
repeat
is the more useful cousin of loop
. It evaluates a given block a predefined number of times, while a loop counter keeps a track of the iterations. The starting value of the counter is 1.
>> n: 4 == 4 >> repeat count n [print["Iteration number" count]] Iteration number 1 Iteration number 2 Iteration number 3 Iteration number 4
Here n
is the number of times to evaluate the block and count
is the iteration counter.
You can think of repeat x y []
as Python’s for x in range(y): …
(Just don’t forget that Red starts the repeat counter at 1):
>>> for n in range(5): print(n) 0 1 2 3 4
When it’s not known in advance how many times a loop will be exexcuted, it’s a good idea to use while
or until
. while
takes a condition-block and a body-block as arguments and evaluates the body as long as the condition evaluates to truthy value.
>> text: "Red is a next-gen programming language, strongly inspired by REBOL " == {Red is a next-gen programming language, strongly inspired by REBOL } >> while [not empty? text][print take/part text index? find text space] Red is a next-gen programming language, strongly inspired by REBOL
In the example above the condition block is [not empty? text]
- we simply check if there are still some characters left in text
and if there are, we evaluate the body. We find the next space in text
, get its index, and take (remove) and print this part (substring) of text
. After each step the condition is checked and if it’s still true, the body is evaluated again.
while
loop in Red is analogous to Python’s while
loop.
In contrast to while
, until
requires only one block. The block is evaluated until the last value in the block is true. This means that the block is executed at least once, whereas with while
the execution of the block can be skipped altogether.
Let’s‘ see until
in action with the follwong example. It demonstrates a simple conversion of a number from decimal to binary number system:
num: 13 base: 2 digits: copy [] until [ insert digits num % base zero? num: to-integer num / base ]
>> probe digits [1 1 0 1]
num
is the number we want to convert to binary, base
is the new number system base (changing it allows for the code to work for other number systems) and digits
is an empty block that will contain the digits after conversion. We start a until
loop, find the remainder from dividing the current value of num
to base
and insert the remainder to the head of digits
(I inserted the elements instead of appended them because otherwise I would need to reverse the block at the end). Then I find the new value of num by dividing it by base
and casting it to an integer. The last value in the block is the result of checking num
against 0
with zero?
. If it is false (num
is not zero), we are not finished yet, so we loop again with the new value of num
; otherwise we exit the loop.
Well, forever
does what it’s name implies – loops through the block that follows and executes it indefinitely.
`foreach is most probably the most frequently used looping construct in Red. It expects three arguments as follows:
foreach 'word series body
word
is a word (or block of words) that is set to the current value of the series at each iteration. series
is the series to iterate and body
is a block of expressions to be evaluated at each iteration.
>> foreach toy ["car" "doll" "puppy" "robot" "teddy bear"][print toy] car doll puppy robot teddy bear
When word
is a block of words, that many values are taken from the series current index onward and the words are assigned that values.
>> foreach [toy qty] ["car" 2 "doll" 3 "puppy" 1 "robot" 1 "teddy bear" 2][print [toy ":" qty]] car : 2 doll : 3 puppy : 1 robot : 1 teddy bear : 2
I’m sure you have already guessed that Red’s foreach
is almost analogous to Python’s for var in iterable: code
:
>>> for toy in ["car","doll","puppy","robot","teddy bear"]: print(toy) car doll puppy robot teddy bear
There is a slight, but important, difference though. When in Red we use a block of words, at each iteration foreach
takes exactly as many values from the series as the number of words in the block. That is, the series is treated as a flat list and the structure of each value is not important. At the opposite, when we use for
with two or more variables in Python, the list is expected to be constructed of lists/tuples, each with exactly the same number of items as there are variables:
>>> for toy,qty in [("car",2),("doll",3),("puppy",1),("robot",1),("teddy bear",2)]: print(toy,qty) car 2 doll 3 puppy 1 robot 1 teddy bear 2
Python’s for
consumes exactly one value from the iterable at each step, no matter how many variables/items in the substructure.
We saw that foreach
traverses the series, getting one value from it at a time. We may think its functionality is similar to this code:
toys: ["car" "doll" "puppy" "robot" "teddy bear"] repeat idx length? toys [ print[toys/:idx] ] --- car doll puppy robot teddy bear
In fact, the index/position in the series is not explicitly known with foreach
- all we know is the current value of the series (of course we can add a counter and update it at each iteration, but that’s different story).
That’s why Red has another looping function - forall
- that exposes the series at each iteration.
forall 'word body
evaluates the body for all values in the series (‘word refers to series we want to iterate over).
Did you notice the '
in front of word
in the function spec? This is very important. As you remember, I used a literal block in the foreach
example. I could have set a word to refer to it and use it as an argument to foreach
, that’s completely fine. There is no freedom of choice with forall
- it must be a word
and cannot be a literal block! The reason is simple – we use the word itself to refer to the series at each iteration (there is no additional word to set).
What forall
does is start at the head and go to the next series at each iteration:
>> forall toys [probe toys] ["car" "doll" "puppy" "robot" "teddy bear"] ["doll" "puppy" "robot" "teddy bear"] ["puppy" "robot" "teddy bear"] ["robot" "teddy bear"] ["teddy bear"] == ["teddy bear"]
Did you observe the difference with foreach
? In our example toys
refers to the entire series, not to a particular value at some index. This means that at each iteration we know where exactly in the series are we.
The above example can be written in Python using for
loop with range()
from 0 to the length of the list and a slice starting at the current loop variable at each iteration:
>>> toys = ["car","doll","puppy","robot","teddy bear"] >>> for idx in range(len(toys)): print(toys[idx:]) ['car', 'doll', 'puppy', 'robot', 'teddy bear'] ['doll', 'puppy', 'robot', 'teddy bear'] ['puppy', 'robot', 'teddy bear'] ['robot', 'teddy bear'] ['teddy bear']
We can easily use all series navigation functions with toys
- next back index? head? tail?
etc. inside the body of a forall
loop:
>> forall toys[print[length? toys tail? next toys]] 5 false 4 false 3 false 2 false 1 true
forall
loop is very useful when we want to change all the values of the series in place. Let’s consider the following simple case: we have a list of numbers and want it updated after multiplying each of them by some coefficient.
>> a: [2 3 11 7 16] == [2 3 11 7 16] >> coef: 2.5 == 2.5 >> forall a[a/1: coef * a/1] == 40.0 >> a == [5.0 7.5 27.5 17.5 40.0]
Since a
refers to the series as a whole, we need to tell Red that we want the item at the current index to be multiplied by the coefficient – that’s why we used a/1
.
remove-each 'word data body
- traverses data
, sets word
to the current value of data
at each iteration and removes the current value if body
returns truthy value.
words: ["premium" "launch" "false" "minister" "breathe" "dawn" "raw" "earthquake" "grow" "entertainment"] remove-each w words[(length? w) > 6] == ["launch" "false" "dawn" "raw" "grow"]
At each iteration we check if the length of w
is greater than 6. If it is, remove-each
removes the value from words
.
Very often, we traverse a series and gather all or some of the elements (possibly after some transformation) in a new series. We saw that forall
is very handy when we are concerned with all of the series’ elements – we can change them in place. When we want to gather the elements in a new series, one option is to start with an empty series, loop through all of the elements of the existing series and append the ones we are interested in to the newly allocated series:
>> numbers: copy[] == [] >> numbers: collect[loop 10[keep random 50]] == [3 31 17 1 13 4 3 21 48 42] >> numbers: [3 31 17 1 13 4 3 21 48 42] == [3 31 17 1 13 4 3 21 48 42] >> even-nums: copy[] == [] >> foreach n numbers[if even? n[append even-nums n]] == [4 48 42]
An alternative to the above code is to use the collect
- it accepts a block and collects in a new block all the values that have been passed to its keep
function. Let’s rewrite the last example using collect
:
>> numbers == [3 31 17 1 13 4 3 21 48 42] >> even-nums: collect[foreach n numbers[if even? n[keep n]]] == [4 48 42]
We can use /into
refinement of collect
to append the newly collected items into a specified buffer (that can be an existing or a new block), instead of in a new block.
Until now we have been using Red’s predefined functions. It’s now time to learn how to define our own functions.
Functions can be created using one of the predefined words func
, function
, has
, or does
, or using make
with function!
as its first argument (type). Let’s start with a simple example:
square: func[n][n * n]
>> square 9 == 81
We define square
to be a function with one argument n
, that returns its argument squared (multiplied by itself): n * n
. As you can see, the arguments are enclosed in a block that is the first argument to func
. That block is said to be the function’s specification. The second block, the function’s body, contains all the expressions that the function evaluates. The last computed value in the body is returned as a result of the function. If the result should be returned earlier than the last expression in the body due to branching, we can use return
followed by a value/expression.
As we saw, the function specification block contains the arguments that the function needs for its workings. Arguments are the only mandatory fields of the function specification (for functions with agruments). There can be other fields though, that make functions better documented and more powerful.
An optional string that may be used to document the purpose and working of the function. It precedes the arguments.
One or more words, each followed by optional block of the allowed datatypes for the argument, followed by optional string that describes the argument.
Refinements are optional. In the context of functions refinements are symbolic values that are used as modifiers to function’s behavior. They start with forward slash /
, followed by one or more Unicode characters, including punctuation characters from the ASCII subset: ! & ' * + - . < = > ? _ | ~`
. Refinements can have optional documenting string, as well as optional arguments with the same syntax as the ordinary function arguments.
The function specification can have a return section – that is return:
followed by a block with the result’s allowed typesets (TBD).
The optional argument /local may be used to list the words that are local for the function.
Let’s update the square
function with a more detailed specification and add a refinement.
square-c: func[ "Squares the input" n [number!] "Number to be squared" /scale "multiply the result with a coefficient" coef [number!] "multiplier" return: [number!] /local result ][ result: n * n result: result * either scale [coef][1] ]
Let me explain how refinements work with test calls to the function square-c
we just defined:
>> square-c 25 == 625 >> square-c/scale 25 1.25 == 781.25
In the body of our function we first calculate result
to be the argument n
multiplied by itself (squared). Then we multupliy it by either coef
or 1, depending on the presence of /scale
refinement. Note that result
is defined to be local for the function body.
Refinements don’t always need to have arguments - in many cases they work as flags. However, when they have arguments, we can use a slightly different logic. Instead of checking the refinement for existence, we can check directly if its argument exists:
square-c: func[n /scale coef][ n * n * any [coef 1]]
We multiply n
by n
and then by the first truthy value in the block following any
- coef
if exists (if the function was called with /scale
refinemet), otherwise by 1.
Let’s see what are the differences between the four types of functions I mentioned earlier.
The built-in function func
is defined as shorthand for make function!
. All words that are not listed after /local
in the function’s specification are bound to the global context. That means that if we hadn’t marked result
as local in our square-c
function, had we had a word in the global context named result
, it would be set to the value of result
from the square-c
body.
function
is similar to func, but it adds all set-words and words from iterators found in the body to the list of local arguments except the ones that occur in a block following the /extern
refinement
has
defines a function without other arguments than local words, thus has [<arguments>] <block>
is equivalent to func [/local <arguments>] <block>
.
routine
is a function with a Red specification and Red/System body. The routine specification takes Red datatypes as arguments and return value, and automatically converts them to appropriate Red/System types when called. A program that contains routine
values can not be interpreted, it must be compiled since it contains Red/System (low level) code.
op!
values are infix functions of exactly two arguments. They are constructed from existing functions, including native!
, action!
and routine!
. op!
takes evaluation precedence over other function types.
Let’s demonstrate the creation of an op!
value with a simple range
function:
..: make op! function[a b][ collect[ while[a <= b][ keep a a: a + 1 ] ] ]
We define ..
to be an op!
, constructed from a function of two arguments. The function itself just loops over and collects the integer values that are in the closed interval formed by the function’s arguments.
>> 1 .. 5 == [1 2 3 4 5] >> foreach n 40 .. 42 [print n] 40 41 42
The Parse dialect is an embedded domain-specific language (DSL) of Red that allows concise processing of input series with grammar rules.
Coming from Python, you are most probably familiar with regular expressions. Regexes are sequences of characters that specify search patterns, usually used by string-searching algorithms for "find" or "find and replace" operations on strings. Red Parse
provides this string-level functionality and much more – it works not only with any-string!
, but also with binary!
and any-block!
series. That means that parse
is capable of parsing blocks of Red values too.
The usage is as follows:
parse input rules
.
input
is any series!
value except for image!
and vector!
. rules
is a block!
value with valid Parse dialect content (top-level rule).
By default, parse
returns logic!
value to indicate whether or not provided grammar rules succeeded in fully matching the input series. However, it is possible for parse
to return a block!
instead of logic!
value, when collect
rule is used.
Also by defualt parse
is case-insensitive, following Red semantics. We can use the /case
refinement to enable the case-sensitivity.
The working of parse
rules depends on the type of input series. When parsing any-block!
, matching by character set has no meaning and always fails. When the input is of any-string!
type, matching by datatype or type set is not supported. For binary!
the matching by datatype or type set is supported for UTF-8 encoded values; such match succeeds if matched portion of the input represents one of the datatype’s literal forms, blank characters before tokens are automatically skipped.
Please refer to Parse documentation for complete and more formal description of parse
.
I’ll give some basic examples of parsing strings, blocks and binaries.
>> parse "Mississippi" ["Mi" 2 "ssi" "ppi"] == true
Here we parse the string "Mississippi" with a block of rules as follows:
-
"Mi" – a literal string
-
2 "ssi" – exactly two consecutive "ssi" ("ssissi")
-
"ppi" – a literal string
>> parse "Red lang" ["Red" space ["f" | "l"] "ang"] == true
-
"Red" – a literal string
-
space
-
["f" | "l"] – a subrule that introduces two alternatives: "f" or "l".
-
"ang" – a literal string
When parsing strings, instead of numerous "|", we can use charsets:
>> f-or-l: charset "fl" == make bitset! #{0000000000000000000000000208} >> parse "Red fang" ["Red" space f-or-l "ang"] == true
f-or-l
rule checks if at the current position is a single character from the charset "fl".
Suppose we want to validate a hypothetical serial number that consists of three groups of four hexadecimal digits, separated by hyphens, for example 1A2B-3C4D-5E6F
digits: charset "0123456789" letters: charset "abcdefABCDEF" hex: union digits letters group: [4 hex] ser-num: [group "-" group "-" group] str: "1A2B-3C4D-5E6F"
>> parse str ser-num == true >> parse "1A2B-3C4D-5E6." ser-num == false
Let’s change our parsing rule so that it allows the groups be composed of 3 or 4 hexadecimal digits. We only need to change the group
rule to [3 4 hex]
- now it looks for 3 to 4 (inclusive) digits.
>> parse "A23B-3C4-5E4" ser-num == true
When a part of the input is not necessarily present (it is optional), we can use opt
before the rule – it means that the rule is repeated zero or one times. any
corresponds to zero or more repetitions of the rule; some
- one or more repetitions. Lookahead is done using ahead
.
Let’s try to implement a rule that validates a floating point number.
zero: charset "0" non-zero: charset "123456789" digit: union zero non-zero float: [ opt "-" [digit ahead dot | non-zero some digit] dot some digit ]
>> parse "-0.23" float == true >> parse "-.23" float == false >> parse "10.35" float == true >> parse "03.35" float == false
For this exercise, I decided to treat the floating numbers having an unnecessary leading zero as invalid, is it’s seen in the last example. So, we start with an optional "-" (` opt "-"`), then we check for either a single digit, followed by a dot (digit ahead dot
) or digits that don’t start with a zero (non-zero some digit
). The rest is trivial - dot
followed by one or more digits.
When we want to advance the position one step forward, we use skip
.
>> parse "abc"["a" skip "c"] == true >> parse "a.c"["a" skip "c"] == true
skip
matches any value and could fail only if the input position is at the tail of the series.
When it is necessary to skip more than one position, we can use to
or thru
.
delimiter: [space | tab | lf | cr | end] text: {Red is a full-stack programming language} print-words: [ any [ any delimiter copy word to delimiter (print word) any delimiter ] ]
>> parse text print-words Red is a full-stack programming language == true
I used the opportunity to introduce a couple of new concepts besides to
with this example. What it does is print the words in a string, where the words are separated from one another with whitespaces. Let’s go through it. delimiter
is rule that introduces the white spaces – space, tab, linefeed or carriage return. I’ve also added end
to them, so that the end of the input is taken into account. The rule`print-words` consists only of any
, followed by a block as its rule. This rule will be matched one or more times. It works as follows – first we go thru any whitespaces with any delimiter
, then copy word to delimiter (print word)
advances the input position to the next whitespace. Here’s the first new concept - copy word
copies to word
the result of the rule that follows to delimiter
. So this part pf the code effectively extracts the words. Please note that copy
in parse
is different from copy
outside of the DSL. What follows is (print word)
. This is very powerful concept in parse
- parentheses enclose a code that is treated as regular Red code and not parse
. This way the code in parentheses is evaluated if the rule preceding it succeeds. Finally there is one more "any delimiter".
When we parse blocks, we work with Red values. Let’s start with a simple example:
>> parse [2 + 3][integer! word! integer!] == true
The block we are parsing consists of three values, that’s why we can use three parsing rules in the rules block. Note that ` is just a word until is evaluated, when it becomes the `
operation (op!
).
Let’s make a parse rule that validates a block, representing a binary operation in the form a op b
, where a
and b
are literal numbers, and op
is an op!
value (an infix function of two arguments).
bin-op: [ number! o: skip if (all[word? o/1 op? get o/1]) number! ]
>> parse [2 * 17] bin-op == true >> parse [a ** 3] bin-op == false >> parse [4 print 3] bin-op == false
We start our rule with number!
- it matches one value, that is a literal number. Then we have o: skip
- this sets the word o
to the current input position and skips the next value. o
is a series, o/1
refers to the skipped value. What we do next is check whether the type of the skipped value is op!
- if (all[word? o/1 op? get o/1])
. Here if
is part of parse
, and not of the regular Red. It succeeds if a given Red expression evaluates to true and never advances the input (we already advanced the input with skip
). Remember that +
is just a word? That’s why we first check if o
is of type word!
(word? o
), and if it is, does this word refer to an op!
value (op? get o/1
). The rule end with the right operand, a number!
.
Let’s finish our short introduction to parsing blocks with another example – let’s check if a block consists of a single value, repetead one or more times. (Of course there is a short way to do it without parse
- single? unique block
).
rpt: [set w skip (r: reduce['quote w]) any r]
>> parse [42 42 42 42] rpt == true >> parse [42 42 43 42] rpt == false >> parse [+ + + +] rpt == true >> parse [+ + + - /] rpt == false >> parse ["Red" "Red" "Red"] rpt == true
Our rule rpt
starts with setting a word w
to the first value at the input start and skipping it. Next, we escape to regular Red and evalueat the code in the parentheses - r: reduce['quote w]
, that generates a rule r`that we’ll use later. The logic of the rule is to match the literal value we set `w
to refer to. For this we will use quote
. quote
uses the value that follows it literally, that’s why we can’t just write quote w
- it would mean that we are going to match literal w
’s. What we do instead is to make a block that consist of quote
, followed by the evaluated value of w
. We could use alternatively r: compose[quote (w)]
. Finally, we repeat the r
rule zero or more times with any r
. If it succeds, then the block consists of a unique element.
binary!
value is a series of bytes, so we can match the input to individual bytes :
>> parse #{CAFE} [#{CA} #{FE}] == true
#{CA} is 202 in decimal; #{FE} is 254, so we can make a bitset!
consisting of these values and match against the bitset:
>> bs: make bitset! [202 254] == make bitset! #{0000000000000000000000000000000000000000000000000020000000000002} >> parse #{CAFE} [any bs] == true
Of course we can match series of bytes:
>> parse #{001234CAFE0000} [to #{CAFE} to end] == true
As we already said, matching by datatype or type set is supported for UTF-8 encoded values (spaces are automatically skipped):
>> parse to binary! "Red: lang" [set-word! word!] == true
You can (and definitely should, if you want to master parse
) check the dedicated resources for learning parse
The Red/View (or just View) is a graphic system for the Red programming language. It provides a Virtual tree of objects as programming interface. The virtual tree is built using face objects; each face object maps to a graphic component on the display in a two-way binding. Face objects are clones of face!
template object. A field in face object is called a facet. Facets include type, offset, size, text. Image color, menu, font, state, as well as many others.
-
Base
-
Text
-
Button
-
Toggle
-
Check
-
Radio
-
Field
-
Area
-
Text-list
-
Drop-list
-
Drop-down
-
Calendar
-
Progress
-
Slider
-
Camera
-
Panel
-
Tab-panel
-
Window
-
Screen
-
Group-box
-
down
-
up
-
mid-down
-
mid-up
-
alt-down
-
alt-up
-
aux-down
-
aux-up
-
drag-start
-
drag
-
drop
-
click
-
dbl-click
-
over
-
move
-
resize
-
moving
-
resizing
-
wheel
-
zoom
-
pan
-
rotate
-
two-tap
-
press-tap
-
key-down
-
key
-
key-up
-
enter
-
focus
-
unfocus
-
select
-
change
-
menu
-
close
-
time
Here’s a short example:
view[ title "View test" txt: field 100x20 on-key-up [btn/text: txt/text] btn: button 100x20 [unview] ]
The code is a block, supplied as an argument to view
dialect. title
sets the title of the window. We then define an edit field named txt
, with dimensions 100 by 20 pixels. on-key-up
defines the behavior when the key is released. btn
is a button with dimensions 100 by 20 pixels. The block that follows describes the actions when the button is clicked – in this case unview
closes the window. Back to txt’s on-key-up
- when a key is released in the edit field, the bnt
text is updated with txt’s text.
Since this is meant to be a short introduction to Red, I’m not going into details of View. Here are some useful links:
Draw is a dialect of Red language that provides a simple declarative way to specify 2D drawing operations. Such operations are expressed as lists of ordered commands (using blocks of values), which can be freely constructed and changed at run-time.
Draw blocks can be rendered directly on top of an image using the draw function, or inside a View face using the draw facet.
The basic drawing sommand are: Line
, Triangle
, Box
, Polygon
, Circle
, Ellipse
, Arc
, Curve
, Spline
, Image
, Text
, Font
.
The pen (the outline drawing of a drawing command) can be one of the following: Color pen
, Linear gradient pen
, Radial gradient pen
, Diamond gradient pen
, Pattern pen
, Bitmap pen
.
The fill-pen (the filling mode for the drawing commands) can be Color fill
, Linear gradient fill
, Radial gradient fill
, Diamond gradient fill
, Pattern fill
, Bitmap fill
.
line-width
, line-join
and line-cap
control the line operations.
Here’s a short example:
my-font: make font![name: "Verdana" size: 30 color: red] view[ base 500x500 white draw[ pen yellow fill-pen red box 10x10 100x100 line-width 5 pen red fill-pen sky circle 250x250 100 pen red font my-font text 400x450 "Red" ] ]
We first create a font!
object to be used later. We then make a GUI, consisting of a simple base
component (think a rectangle) with size 50x500 pixels which color is set to white. The draw
commands are going to be renedered in this base
. What follows is a box
, drawn with yellow pen and filled with red color. The box’s top-left corner is at coordinate 10x10; its bottom-right corner is at 100x100. Then we change the pen to red and the fill-pen to sky in order to draw a circle
centered at 250x250 with radius 100 pixels. Finally, we load the previously declared font
and use it to render a text
at coordinates 400x450.
When we need to use words instead of literal values in a draw
block, we can use reduce
or (more conveniently) compose
. Let’s say we have a word cntr
that refers to a pair 100x100 and radius
that refers to 100. We can use these words in a draw
block and draw a circle with center at cntr
and radius radius
as follows:
draw compose[circle (cntr) (radius)]
Of course the values these words refer to can be changed dynamically.
You can check these links for more information and examples on Draw
:
Both Red and Red/System are published under the BSD license. The runtime is published under the BSL license.