Dry is a dynamically-typed, high-level programming language currently being written in Scala.
The image below shows an overview of Dry's syntax via examples. You can learn more about the language here.
- Introduction
- Installation
- Getting Started
- Running the tests
- Examples and Tests
- General
- Classes and Constructors
- Lists, Tuples, and Dictionaries
- Exception Handling
- Modules
- More here and here
- Grammar
You can think of Dry as a Python-like programming language with curly braces and better support for functional programming (e.g. multi-line lambdas and partial function application). Dry is both dynamically and strongly typed, just like Python.
Dry was also heavily influenced by the Lox language, and you'll see why in the next section.
The name doesn't actually mean "Don't Repeat Yourself" (though maybe it can be a good slogan). Dry was rather named after the eye condition from which I suffered for about 2 years.
Dry started as a hobby project, when I was going through the first part of Crafting Interpreters. This is how Lox, the object language in that book, had also influenced the design of Dry.
However, as Dry started to grow, it became more and more of a language for cases where Python would normally shine. It became a language for people like myself back then (when I used Python at work), who would sometimes wish Python had multi-line lambdas, supported braces and not overly rely on indentations.
Of course Dry doesn't have the libraries and tools that Python has, but if the following are true about you or your requirements, then you might want to give Dry a try:
- You want an expressive language to write scripts, and you don't need the help of a static type system.
- You don't need much tools and libraries for your project.
- You like Python but want to use first-class functions a lot.
-
The easiest way to install Dry on your system is to download a Dry executable (a Jar file) from the releases page. I recommend you choose the one from the latest release. You can put the jar file in a directory of your choosing.
-
Install Java, if you haven't already.
-
Since Dry is a Scala application, it is compiled to Java bytecode. This means you can run the Jar file you downloaded in the previous section the same way you run any Java Jar application, using the
java -jar
command. Note however that Dry programs are not compiled to Java bytecode, only Dry (the interpreter) itself is.Go to the downloaded jar's directory and enter the following:
$ java -jar dry-<version>.jar
where
version
is the version of the release you downloaded. You should see a welcome message in the screen:Welcome to Dry. Type in expressions and statements for evaluation. Type 'exit' to quit. dry>
You are now ready to start playing with Dry.
In Dry, like in some languages, you can either start a REPL, or run a script. This section will show you both.
The welcome message you saw at the end of the installation section indicates that you have entered the REPL (read-eval-print-loop) environment. That's what would happen if you ran Dry without specifying a path to a Dry script.
Many languages such as Scala, Python, Haskell, or any of the known Lisp dialects like Clojure, have their own supports for REPLs, so programmers coming from a background of any of these languages might already be familiar with it.
The REPL allows you to enter expressions and declarations, and see their evaluated results immediately:
dry> 100 + 20 - 8 * 5
80
dry> "hello" + "world"
helloworld
dry> let x = 10;
dry> x * x
100
dry> def sum(x, y) { return x + y; }
dry> sum(10, 9)
19
dry> sum(11, (lambda(y) { return y * y; })(2))
15
dry> let one_plus = sum(1, _); // partial application
dry> one_plus(5)
6
dry> one_plus(6)
7
dry>
To quit the REPL, enter exit
:
dry> exit
Bye!
To run a Dry script, you need to save the script to a file first and pass its path
as an argument to the jar command. For instance, the code you saw at the beginning
of this ReadMe file is available in examples/class_demo.dry
under this repository. We can
try running that with Dry:
$ java -jar dry-<version>.jar <path-to-repo>/examples/class_demo.dry
quack!
Woof
quack!
1 1 2 3 5
As shown above, Dry source files end with the .dry
.
The project has a custom sbt command for running the test:
$ sbt testDry
If everything works correctly, your console should print a bunch of assertion results. The end of the logs should look like this:
[Success] Binding third param
=========== test_modules.dry ===========
[Success] Access variable from imported module
[Success] Access function from imported module
[Success] Modify values from imported module
[Success] Access variable from imported module
[Success] Modify values from imported module
[Success] Access method from module found via Dry Path
============= test_init.dry ============
[Success] Init should save the set variables
[Success] Init should be capable of accepting parameters
============ test_class.dry ============
[Success] Type of class
[Success] Type of instance
[Success] Ducks should quack!
[Success] Denji should say 'Woof!'
[Success] Class properties should be updated
Ran 179 tests. Successful: 179. Failed: 0.
The tests themselves are written in Dry (while the testDry
command is written in Scala). You can see the directory
containing them here: https://github.com/melvic-ybanez/dry/tree/main/tests. All the files in that directory that start
with test_
and have the Dry extension will be picked up by the testDry
command.
There is also a set of tests written in Scala, and you can run them like normal Scala tests:
$ sbt test
If you want to import
a specific module into your Dry program, you need to include the directory containing
that module in the list of available paths. You do that by adding a .dry_paths file in the same directory
as your main Dry script and write all the paths there, each on its own line.
See the .dry_paths file within the tests directory as an example. You might notice it includes the stdlib. That's because I needed the assertion functions for the tests.
The syntax of Dry should be familiar to Python and Scala developers. Here's the grammar, written in BNF:
<declaration> ::= <class> | <function> | <let> | <statement>
<class> ::= "class" <identifier> "{" <function>* "}"
<function> ::= "def" <identifier> <params> <block>
<let> ::= "let" <identifier> ("=" <expression>)? ";"
<statement> ::= <expr-stmt> | <block> | <if> | <while> | <for> | <return> | <import> | <delete> | <try-catch>
<expr-stmt> ::= <expression> ";"
<if> ::= "if" "(" <expression> ")" <statement> ("else" <statement>)?
<while> ::= "while" "(" <expression> ")" <statement>
<for> ::= "for" "(" (";" | <let> | <expr-stmt>)
(<expression>? ";") <expression> ")" <statement>
<return> ::= "return" <expression>? ";"
<import> ::= "import" <identifier>("."<identifier>)* ";"
<delete> ::= "del" <call><index> ";"
<try-catch> ::= "try" <block> ("catch" "(" <identifier>? ":" <identifier>? ")" <block>)+
<expression> ::= <assignment> | <lambda>
<assignment> ::= <call> "=" <expression>
<call> ::= <primary> ("(" (<expression> | ("," <expression>)*)? ")" | "." <identifier> | <index>)*
<index> ::= "[" <expression> "]"
<identifier> ::= <alpha>(<alpha>?<digit>?)*
<lambda> ::= "lambda" <params> <block> | <or>
<block> ::= "{" <declaration>* "}"
<params> ::= "(" (<identifier> | ("," <identifier>)*)? ")"
<or> ::= <and> ("or" <and>)*
<and> ::= <equality> ("and" <equality>)*
<equality> ::= <comparison> ("!=" | "==" <comparison>)*
<comparison> ::= <term> (">" | ">=" | "<" | "<=" <term>)*
<term> ::= <factor> ("-" | "+" | "&" | "|" | "^" | "<<" | ">>"
| ">>>" | "<=" <factor>)*
<factor> ::= <unary> ("/" | "*" | "%" <unary>)*
<unary> ::= ("!" | "-" | "+" | "not")* <call>
<primary> ::= "false" | "true" | "none" | <number> | <string> | "self" | <identifier>
| <list> | <tuple> | <dictionary> | "(" <expression> ")"
<list> ::= "[" (<expression> ("," <expression>*))? "]"
<tuple> ::= "(" (<expression> ("," | ("," <expression>)*))? ")"
<dictionary> ::= "{" (<key-value> ("," <key-value>)*)? "}"
<key-value> ::= <expression> ":" <expression>
<number> ::= <sign>?<nat>("."<nat>)?
<sign> ::= "-" | "+"
<string> ::= '"'(.?"\n"?)*'"'
<alpha> ::= 'a' ... 'z' | 'A' ... 'Z' | '_'
<nat> ::= <digit><digit>*
<digit> ::= '0' ... '9'