29

Nim Language Highlights

 5 years ago
source link: https://www.tuicool.com/articles/hit/iUV3qaq
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In thelast post I covered some things I’ve found compelling about nim ’s big picture.

Today I’ll do the same but with the language itself. If you’re still on the fence about what the big deal is, let me show you a few things I found interesting.

Let’s get scrolling.

Templates

A template is a function which can help you reduce boilerplate and write cleaner code. It’s kinda like a preprocessor in c , except type-safe.

# define a template like a function
template safely(stuff: untyped) =
    try:
        stuff
    except:
        echo "(╯°□°)╯︵ ┻━┻"

let msg = "i am happy!"

# call the template passing a block as the parameter
safely:
    echo msg[0]
    echo msg[2..3]
    echo msg[5..1000]

echo "!!!"

Fun fact from the manual : the != operator is a template which rewrites a != b to not (a == b) . Neat eh?

Macros

When you ask about the killer features of nim , many people will say macros.

Macros operate at the AST level. By evaluating the inbound code syntax, you can perform branching logic, augment it, or even rewrite it entirely. You can use macros to define DSLs or introduce amazing language features like pattern matching and interfaces .

import macros

macro theMagicWord(statments: untyped): untyped =
    result = statments
    for st in statments:
        for node in st:
            if node.kind == nnkStrLit:
                node.strVal = node.strVal & " Please."

echo "Hey!"
theMagicWord:
    echo "Grab me a beer!"

Macros are a big topic and best left for a dedicated post; don’t wait for me though:

Flexible Identifier Names

Alright. Grab your torch & pitchfork.

Identifiers, such as variable names and functions, have flexible names. It’s case insensitive (except for the first letter) and underscores are optional.

proc goodDayToYou(firstName: string) =
  echo "Good day to you " & firstname & "."

# both are ok
goodDayToYou("Joe Camel")
good_day_to_you("Medusa")

Did you catch the firstname vs firstName in the function above? I bet you saw it and laughed at me. You know who else saw it? The compiler; and it knew what I meant.

Is it a big deal? Between nimgrep and nimsuggest (via IDE plugins), discoverability isn’t an issue. You can also rename functions and constants from external c / c++ libraries as they come in, so that’s not a problem either.

It’s just quirky and oddly satisfying when it saves your butt.

EDIT #1: Dominik Picheta points out that the practical purpose for this feature is to have freedom from the naming choices of your dependencies. You’re Team Camel but they’re Team Snake? No problem. Now you can stop avoiding eye contact with fellow devs at conferences.

Flexible Functions Calls

Let’s define a function which clips text at a ridicu…

proc truncate(value: string, length = 6, ellipsis = "..."): string =
    if value.len > length:
        value[0..length - 1] & ellipsis
    else:
        value

How do we invoke it?

Call It Like A Function

As you’d expect, you can call the function like this:

echo truncate("hello world")

Turns out the story doesn’t end here.

Call It Like A Method

The first parameter is a string , so in any scope where the truncate function is available, it is now accessible on any instances of string . Like extensions in swift & c# or mixins in ruby .

echo "hello world".truncate(4)

Now you can operate in a more fluent-style where you chain functions together like a pipe.

TIL this has a name: UFCS .

Parentheses Can Be Optional

Parentheses are optional as long as there is no ambiguity.

# ok
echo "hello WORLD ".toLowerAscii.truncate(4).strip
echo "hello WORLD ".toLowerAscii.truncate.strip

# not ok
echo " hello WORLD ".toLowerAscii.truncate 4.strip

Call it With Named Parameters

You can call parameters by name as well like our obj-c friends.

echo truncate(
    value = "hello there how are you?",
    length = 4,
    ellipsis = "(lol)",
)

# or partially (for optionals)
echo truncate("Morse Code Rulez", ellipsis = "dot dot dot")

# or out of order
echo truncate(ellipsis = "!!!", value = "Stellar stuff.")

Flexible Function Returns

We just saw how to invoke functions, but there’s also flexibility when returning values from a function too: explicit, implicit, and result .

Explicitlyvia return is good for bailing from an early exit.

proc guess(a, b: int): string =
    let x = 1.0
    if a == 0: return "nope"
    if b == 0: return "uh oh"
    $(a / b + x)

Also, implicitly , the last expression evaluated is assigned the return value.

Another way is to assign the reserved result variable:

proc score(a, b, c: bool, marioKartRules = true): int =
    if a: result.inc
    if b: result.inc
    if c: result.inc
    if not marioKartRules:
        return
    if result < 2:
        result.inc 10

A few things to notice here:

result
result
return

Again with the pitchforks? Let me explain.

  1. All procedures are given the result variable for “free”
  2. result is set to the “default” value of appropriate return type – e.g. int = 0 , string = "" (as of 0.19 ), refs are null
  3. when you exit a function, the value of result at time of the return will be its default value

Doc Tests

Doc tests allow you to tie runnable examples of your code alongside your documentation. The premise is that you’ll keep your docs up-to-date if you can’t generate them without passing the tests.

rust and elixir have this, as I’m sure other languages do. This concept is fairly new to me, though.

proc encrypt*(clearText: string): string =
    ## Pass a clear text value to encrypt a password using
    ## the Windows 3.1.1 for Workgroups algorithm.
    ##
    ## Example:
    runnableExamples:
        doAssert "password".encrypt == "********"
        doAssert "".encrypt == ""

    if cleartext.len == 0:
        ";)"
    else:
        "*".repeat(clearText.len)

Now when you run nim doc wfw311_crypto.nim , the runnableExamples block will kick in and cause a failure because the 2nd assertion should be ;) .

Heads up: this only works with exported functions.

By the way, did you notice that I snuck another cleartext vs clearText ? No you didn’t. Liar.

Exporting

A .nim file is module and what happens inside a module stays inside a module. Your functions, constants, and variables are isolated until you tell the compiler otherwise.

That is done with the * suffix.

# securitay.nim
let privateKey = "p@ssw0rd"
let publicKey* = "password"

publicKey is available from other files (e.g. from securitay import publicKey ), but privateKey is not.

If you’re familiar with javascript , it’s like using export (minus the gotchas). The * identifier feels cryptic at first, but it grows on you. Now I wish I had it back in javascript .

The same technique is true for properties on custom type s.

type
    User* = object
      username*: string
      password: string

See how password is missing a * ? That means, outside this file, anyone who uses the user.password will get an error. This works like the fileprivate modifier in swift .

Defer

I first saw this in go . It allows you to queue up code to execute before the local scope finishes.

proc backupDb() =
    let db = openDb()

    # this will always run as the last thing
    # this function does
    defer:
        db.close()

    # even if you do stuff like this:
    db.lockTheUserTable()
    raise newException("party-parrot")

Admittedly, I expected to use this feature more than I do.

Iterators

Iterators are functions that return more than once. You use them in for statements.

They are defined like a proc except use the iterator keyword. Instead of return ing values once, you yield values multiple times.

type
    Member* = tuple[name: string, original: bool]

iterator aceOfBaseGroup(): Member =
    yield ("linn", true)
    yield ("jenny", true)
    sleep 500 # simulate some work
    yield ("ulf", true)
    yield ("jonas", true)
    yield ("clara", false)
    echo "i saw the sign!"

for name, original in aceOfBaseGroup():
    if not original:
        break
    echo name

In this example, we’ll never see the sign in the terminal because of the break statement.

You can use them in long running processes where you have multiple steps or need a progress bar. They’re also great for querying nested data structures, for example for p in paragraphTags(dom) .

staticExec and staticRead

With staticExec you can read a string in from a command at compile time and use it at runtime. staticRead does the same, but reads from a file.

# static.nim
const lines = staticExec("cat static.nim | wc -l")
const source = staticRead("static.nim")
echo source
echo lines & " lines!"

This is nice-to-have since you won’t have to roll your own build tools for this purpose.

when isMainModule

The when keyword can be used for conditional compilation. For example, you can detect platforms, OSes, compiler defines/flags (like nim -d:omg ) and more. Nothing new here.

I was a bit surprised to see a lot of people using this for writing tests within the same module. Maybe I’ve spent too much time in javascript land though.

# lies.nim
proc add(a, b: int): int =
    a + b + 1

when isMainModule:
    # this will fail, but only if you run this file
    doAssert add(1, 1) == 2

You can make a module contain the implementation, documentation, and tests. All as a single file. That’s kinda cool, although I’m not entirely sure how I feel about it just yet.

Asynchronous IO

Like Node.JS, nim has an async io mode. You can decorate your functions with the {.async.} pragma and then await on future-based functions inside. Great for writing servers .

import asyncdispatch

proc alex() {.async.} =
    echo "this"
    await sleepAsync 1000
    echo "is"
    await sleepAsync 1000
    echo "jeopardy!"

waitFor alex()

Many of the libraries have async versions such as sockets, http clients & servers, and file-system.

Pragmas

Pragmas are declarative markers which attach to things like functions, properties, types, statements and more. If you’re familiar with decorators in typescript or attributes in C# then you know what these are. It’s very meta.

They are enclosed within {. and .} and multiple pragmas will be comma-separated. They can also take parameters by passing a : after the name.

Here’s a few examples of some pragmas found in the nim standard library.

{.emit.}
{.deprecated.}
{.noSideEffect.}
{.inline.}
{.raises.}
{.importc.}

And here’s an example of {.header.} , {.importc.} , and {.varargs.} working together to import a function from a c library.

proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}

There are dozens of these built-in that do everything from provide hints to the compiler, generate code, modify the linking steps, and more.

You can build your own.

The {.emit.} Pragma

The docs say “don’t do it” and “sloppy” and “highly discouraged”.

OTOH, when someone hands you a big red button labeled “Don’t Press” , you press it.

Ladies and gentlemen, I present… the golden escape hatch:

# c_in_nim_to-c.nim
proc showtime() =
    let emote = "weeee!".cstring
    {.emit: """
        printf("Straight to hell I go, %s!\n", `emote`);
    """.}

showtime()

And it gets better .

Effect System

nim has ways to give the compiler more information about the expectations of your functions. If you want to disallow side-effects or ensure no exceptions can be raised, you can do this through the effect system.

For example, this won’t compile:

# echo writes to the terminal ... this is a side-effect
proc sayHello(name: string) =
    echo "hello " & name

# a func is a proc that can't have side effects
func hi =
    sayHello "steve"

And There’s Still More

I’ll stop here, but the list keeps going!

I didn’t talk about any of the impure libraries (ones that rely on 3rd party libraries) such as postgresql , openssl , pcre . I didn’t cover any of the parsers, threading, data structures, or string utils. if and switch statements are expressions. nim secret repls…

The official tutorial and docs have more to explore when you’re ready to dig in.

I still have the training wheels on for this language, but I’m doing c bindings now and writing lexers and parsers. Trust me. I’m not a lexer and parser kinda guy. You can probably tell. :)

I’m finding nim comfortable and approachable despite the huge feature set.

Batteries included, as they say.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK