Skip to content

Instantly share code, notes, and snippets.

@RNGKing
Last active March 17, 2024 04:31
Show Gist options
  • Save RNGKing/76561abecb8eae3448f308093f1e9698 to your computer and use it in GitHub Desktop.
Save RNGKing/76561abecb8eae3448f308093f1e9698 to your computer and use it in GitHub Desktop.
Learn X in Y Minutes Gleam Proposal!
language contributors filename
gleam
learngleam.gleam

Introduction

Gleam is a functional programming language with strong typing capable of targeting multiple runtimes, notably the BEAM runtime and Javascript (web, deno and node). It is fully compatible with erlang and javascript, but features static typing that allwos for an expressive type-safe and enjoyable programming experience.

This document is intended to be a "ctrl-c, ctrl-f" document for quick reference if you forget how to do something. For a complete and interactive tour, please visit the Language Tour!

Getting Started

To get started type n your shell of choice :

gleam new hello_world

Then change directories to the project's root folder

cd hello_world

To run a basic gleam program type:

gleam run

Congratulations! You've ran your first program using Gleam!

Lets Get To Some Code

// -- COMMENTS AND DOCUMENTATION STRINGS
// Single line comments start with double forward slash

// There isn't a multi-line comment,
// But you can have multiple per file

// Documentation strings that document the entire module start with ////
// and use markdown for formatting

//// This is some important information, don't dereference a null pointer!

// Documentation strings that document the particular statement start with ////

//// this is an example document string
fn do_something() -> Int {
    // we do stuff here
}

// Comments and documentation strings can also contain markdown for a better formatting experience

// -- VARIABLE DECLARATION

let x = 5

// Variables in Gleam are immutable, meaning that once you bind them with "let" 
// they cannot change 
// This will become more apparent later on

// Types in Gleam are inferred at compile time, however they can be explicit

// Explicit example
let x: Int = 5

// Implicit example
let x = 5

// A NOTE ON NAMING CONVENTIONS

// Variable declarations are done with lower case letters, stylistically snake cased
// Type declarations are done with Camel casing

// -- BASIC MATH OPERATORS

// Addition
let sum = 2 + 2

// Subtraction
let difference = 2 - 2

// Multiplication
let product = 2 * 2

// Division
let quotient = 2 / 2

// BASIC STRING CONCATENATION
let message = "Hi John! " <> "Welcone to the U.S.A!"

// This leads us into the concept of:

// -- TYPES!

// Types are declared after a variable is named using the let keyword:
// EX: let x: Int = 5
// The variable is labeled as "x", and is assigned the Type of Int,
// This means that x can never be transformed into any other type and this 
// will be validated at compile time

// For example:
let x: Int = 5
let y: Float = 10.5
let output = x + y // COMPILER ERROR, you can't add an Int to a Float!

// STANDARD LIBRARY TYPES
// Gleam has a small number of basic types. They are as follows:

// String -> A "string" of letters and characters
let greeting: String = "Hello, John!"

// Boolean -> True/False
let the_cake_is_a_lie: Bool = False

// Int -> Non-Decimal numbers
// Can be represented as whole, binary, octal, and hexadecimal
let chicken_count_whole: Int = 25 // whole number
let chicken_count_binary: Int = 0b00001111 // binary number
let chicken_count_octal: Int =  0o17 // octal number
let chicken_count_hex: Int =  0xF // hex number

// Float -> Represents decimal numbers ( Please do not confuse this with a "decimal type" )
let gleam_stonk_price: Float = 6495.45

// List -> Collection of a singular type
// Lists are homogenous, meaning that they cannot be made up of multiple types
// Valid:
let cat_names: List(String) = ["Natty", "Sock", "Mittens", "Anderson"]
// Invalid:
let cat_stuff = ["Natty", 54, 74.405] // Compiler Error

// Tuple -> Collections of mulitple types
// Tuples are fixed length collections of multiple types
let cat_data :  #(String, #(Float, Float)) = #("Natty", #(54.34, 83.26))

// Byte Array -> Collection of bits represented as "chunks" of bits
// When declaring a byte array you must declare the bit segment type or length of segment
let returned_string: BitArray = <<"Wow, this is such a great language!":utf8>> 

// You can represent ANY binary data this way, not just characters
let returned_data: BitString = <<55642345:3>>

// Result -> Catch faults early and express them as a Result type
let network_result: Result(Int, String) = Ok(55) // Ok state
let bad_network_result: Result(Int,  String) = Error("Timeout when accessing url: https://get_a_cat/cute") // Error state

// Custom Types
// Not all information can easily be expressed as basic types.
// Gleam allows you to build custom types to associate different pieces of data together
type Cat {
    Cat(name: String, age: Int, fur_color: FurColor)
}

// Custom types can also represent "sums of different types."
// For example, we can represent a fixed set of colors using this technique
type Color {
    Red
    Blue
    Green
    Yellow
    Lavender
    Indigo
}

// Then we can declare a color using the let keyword
let red: Color = Red()
let lavender: Color = Lavender()

// We can treat the variables red and lavender as if they were a Color!

// You can even store varying data types within the same type
type Point {
    Point2D(x: Float, y: Float)
    Point3D(x: Float, y: Float, z: Float)
    PointComplex(real: Float, imaginary: Float)
}    


// The use of this pattern will become more apparent when we
// discuss Case Expressions

// -- ACCESSING VALUES WITHIN A TYPE
// You can access a value within a type by using the common "."
type Cat{
    Cat(name: String, age: Int, fur_color: FurColor)
}

let tabby: Cat = Cat("Natty", 6, Tabby)
let cat_age: Int = tabby.age // -> We get the age of the cat here! This is a new value though, we cannot mutate "tabby"'s age with this 
// reference

// -- OPAQUE TYPES
// Sometimes you want to express a type but not give visibility to the values within.
// You can do so the following way:

pub opaque type Cat{
    Tabby(name: String, age: Int, fur_color: FurColor)
}

// Imagine if we were in a seperate file and we try to access the opaque cat type
// We'll discuss what the "fn" and "case" mean later in the guide
fn cat_magic(cat: Cat) {
    case cat.fur_color {
        Tabby() -> "Tabby Blast!" 
        ...
        _ -> "Default Case"
}

// This would fail to compile because we're trying to access the opaque types internal values. 

// -- DESTRUCTURING TYPES
// You can "destructure" types, lists and tuples to get access to objects within them.
type Cat {
    NamedCat(name: String, age: int),
    NoNameCat(age: int)
}

fn returns_just_cat() -> Cat {...}
let NamedCat(cat_name, cat_age) = returns_just_cat()

// -- CASE EXPRESSIONS
// Case expressions let you add branching. It does this with "pattern matching" 
// which is hard to explain in this quick format. Just know that the case block
// tries to "match" your input with the declared branches.
// Cases must be EXHAUSTIVE, meaning all possible outcomes must be handled
let cat_age : Int = 12
case cat_age {
    0 -> io.println("Your cat is just a kitten")
    1 -> io.println("Your cat is 1 year old")
    age -> io.println("Your cat is old! They're " <> age <> "!")
    _ -> io.println("Invalid case")
}

// Cases also let you destructure variables
// you can use destructuring to get access to elements in a list!
let cat_names : List(String) = ["Natty", "Sock", "Mittens", "Anderson"]
case test_list{
    [head | ..tail ] -> "Cat name is : " <> head
    [] -> "No more cats!"
}

// head is taken off of the top of the list and the rest of the list is put into tail
// In this example the value of the variable head here is "Natty"

// You can match against strings
case string_val {
    "test_case" -> do_test()
    "hey now " <> string_val -> do_other_test()
    _ -> "default case" 
}

// You can match with if-blocks
case xs {
  [a, b, c] if a == b && b != c -> "ok"
  _other -> "ko"
}

// Cases also let us pattern match on custom types, lets use our set of points

type Point {
    Point2D(x: Float, y: Float)
    Point3D(x: Float, y: Float, z: Float)
    PointComplex(real: Float, imaginary: Float)
}

type Rotation{
    Rotation2D(angle: Float)
    Rotation3D(rot_x: Float, rot_y: Float, rot_z: Float)
}

type Scale{
    Scale2D(scale_factor: Float)
    Scale3D(x: Float, y: Float, z: Float)
}

type Transform{
    Transform2D(position: Point2D, rotation: Rotation2D, scale Scale2D)
    Transform3D(position: Point3D, rotation: Rotation3D, scale Scale3D)
}


let cat_pos: Point2D = Point2D(x: 25.4, y: 85.6)
let cat_rot: Rotation2D = Rotation2D(angle: 45.6)
let cat_scale: Scale2D = Scale2D(1.0)
let cat_transform: Transform = Transform2D(position: cat_pos, rotation: cat_rot, scale: cat_scale)

case cat_transform {
    Transform2D(pos, rot, _) -> print_2d_transform(pos, rot)
    Transform3D(pos, rot, scale) -> print_3d_transform(pos, rot, scale)
}

// In the given example, the case statement takes in the Transform type
// and matches the possible cases, Trasnform2D and Transform3D
// It then will evaluate the Transform2D branch because the underlying
// type is a Transform2D

// -- FUNCTIONS

// Functions are laid out in the following form:
fn add_two_numbers(x: Int, y: Int) -> Int {
    x + y
}

// And can be invoked like this:
let x = 5
let y: Int = 10
let output = add_two_numbers(x, y)

// the "fn" declares the start of a function declaration.
// You then provide a name for the function followed by a set of parenthesis.
// In between the parenthesis you can put the arguments that will be used within the function.
// The "->" defines an explicit return type.
// The last statement of a function is what is returned.
// Function declarations can have inferred types for both the return value
// and the arguments.

//  -- PIPE OPERATOR
// The pipe operator lets you chain the output of a function into the first argument of another.
fn process_users_of_name(name: String) -> Result(List(User), String){
    case sql_get_users_first_name(name) {
        Ok(result_list) ->{
            let output = result_list
                         |> map(fn(x) { x <> " Squarepants" })
                         |> filter(fn(x) { string.contains(x, "Daisy")})
            Ok(output)
        }
        Err(msg) -> Err(msg)
    }
}

// -- PUBLIC VS PRIVATE
// You may have noticed when we were describing types and functions the "pub" keyword that may or may
// not have been there. The "pub" keyword allows for your function to be visible outside 
// of the gleam file, without it the type or function definition will not be able to be accessed.

pub fn publically_available() {
    // This function can be used by external code
}

fn private_function() {
    // This function can't be accessed by outside code
}

pub type PublicCustom{
    PublicCustom()
}

type PrivateCustom{
    Custom()
}

//  -- IMPORTING MODULES

// In Gleam (and Erlang!) code can be broken up into distinct modules
// Gleam does this using files, so do_this.gleam and do_that.gleam are 
// considered seperate files.

import gleam/erlang
import my_folder/my_custom_gleam_file

// -- IMPORTING EXTERNAL CODE FROM ERLANG OR JAVASCRIPT

// A lot of the time there isn't a library in Gleam already made 
// to handle your particular need. But that's ok because we can utilize
// external erlang or javascript libraries to handle this!

@external(erlang, "eg_pdf", "export")
pub fn export_pdf(pid: Pid) -> {Int, Bitstring}

@external(javascript, "./my-module.js" "run")
pub fn run() -> Int

// -- DISCARD SYMBOL
// When destructuring or handling the "any" condition in a case statement, 
// if you wish to ignore the value you use "_"
// This does not "dispose" of the memory, but tells the runtime to not care about the value.

// -- LOOPING
// Gleam is a functional language and lacks traditional loops. Instead, we use recursion. 

pub fn do_task_from_list(tasks : List(String)) -> Result(String, String){
    case tasks {
        [task | ..tail] -> {
            case handle_task(task){
                Ok( _ ) -> do_task_from_list(tail)
                Err( msg ) -> Err(msg)
            }
        }
        [] -> Ok("We did it!")
        _ -> Error("We can't get here but this is an example of the discard symbol!")
    }
}

Some Final Notes

With the gleam command line tool, we can do so much more.

Add dependencies from hex.pm

A link to the hex page.

gleam add gleam_erlang

Get access to the Erlang REPL, letting us evaluate our code in our command line interface

gleam shell

Create documentation from the doc strings

gleam docs build

Conclusion

There you have it. A brief tour of the Gleam programming language. With this knowledge you are now dangerous!

Further Reading

  1. Gleam Home Page
  2. Language Tour
  3. Learn You Some Erlang for Great Good
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment