Project

rfunk

0.0
Repository is archived
No commit activity in last 3 years
No release in over 3 years
See https://github.com/alexfalkowski/rfunk/blob/master/README.md
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

~> 1.1, >= 1.1.7
~> 3.0
~> 0.11.2
 Project Readme

rfunk

The concepts behind this gem are not new. They have been around for decades.

Option/Maybe

Maybe represents computations which might "go wrong", in the sense of not returning a value. Why is this important?

Tony Hoare apologized for inventing the null reference:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

How do we use this?

RFunk.option('value') == RFunk.some('value')
RFunk.option(nil) == RFunk.none

RFunk.option('value').or('other') == RFunk.some('value')
RFunk.option(nil).or('other') == RFunk.some('other')

Either

Either allows us to incorporate a context of possible failure to our values while also being able to attach values to the failure, so that they can describe what went wrong or provide some other useful info regarding the failure

How do we use this?

RFunk.either(-> { 'YES' }) == RFunk.success(RFunk.some('YES'))
RFunk.either(-> { 1 / 0 }) == RFunk.failure(RFunk.some(ZeroDivisionError))
RFunk.either(nil) == RFunk.failure(RFunk.none)
RFunk.either(RFunk.some('YES')) == RFunk.success(RFunk.some('YES'))
RFunk.either(RFunk.none) == RFunk.failure(RFunk.none)

RFunk.either(-> { 'success' }).or('failure') == RFunk.success(RFunk.some('success'))
RFunk.either(-> { 1 / 0 }).or('error') == RFunk.failure(RFunk.some('error'))
RFunk.either(nil).or('error') == RFunk.failure(RFunk.some('error'))
RFunk.either(nil).or(nil) == RFunk.failure(RFunk.none)

Lazy

Lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed.

How do we use this?

lazy = RFunk.lazy(-> { 'Lazy' })
lazy.value == RFunk.some('Lazy')
lazy.created? == true

lazy = RFunk.lazy(-> { nil })
lazy.value == RFunk.none
lazy.created? == true

lazy = RFunk.lazy(-> { 'Lazy' })
lazy.created? == false

Immutability

Mutability has been one of the biggest downfalls in OO programming. Why is this important?

Rich Hickey has a great way of looking at this:

In OO, there is no real clear definition of state. Maybe it's, "the values of all the attributes within an object right now." And it has to be "right now", because there's no language-supported way of holding on to the past. This becomes clearer if you contrast it with the notion of identity in FP. In the Hickeysian universe, a State is a specific value for an identity at a point in time.

Immutable Classes

class Customer
  include RFunk::Attribute

  attribute :first_name, String
  attribute :last_name, String
end

customer = Customer.new

customer.first_name == RFunk.none
test_customer = customer.first_name('test').last_name('test')
test_customer.first_name == RFunk.some('test')
test_customer.last_name == RFunk.some('test')
test_customer.first_name = 1 == RFunk.failure(TypeError, "Expected a type of 'String' for attribute 'first_name'")

customer = Customer.new(first_name: 'test', last_name: 'test')
customer.first_name == RFunk.some('test')
customer.last_name == RFunk.some('test')

Immutable Values

This keyword has an alias of let.

class Customer
  include RFunk::Attribute

  fun :full_name do
    val name: 'Alex'

    value(:name)
  end

  fun :immutable_full_name do
    val name: 'Alex'
    val name: 'Alex'

    value(:name)
  end
end

customer = Customer.new
customer.full_name == RFunk.some('Alex')
customer.immutable_full_name == RFunk.failure(ImmutableError, "Could not rebind a value '[:name]', because they are immutable.")

Design by Contract

As stated by Wikipedia

Design by contract (DbC), also known as contract programming, programming by contract and design-by-contract programming, is an approach for designing software. It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.

How do we use this?

fun :say_hello do |name|
  pre {
    assert { name == RFunk.some('Bob') }
  }

  body {
    val return: "Hello #{name}!"
    value(:return)
  }

  post {
    value(:return) == RFunk.some('Hello Bob!')
  }
end

Types

RFunk has the ability to specify types as a part of a function definition.

Return Types

fun :say_hello => String do |name|
  pre {
    assert { name == RFunk.some('Bob') }
  }

  body {
    val return: "Hello #{name}!"
    value(:return)
  }

  post {
    value(:return) == RFunk.some('Hello Bob!')
  }
end

If the return type is not a string we would get the following error:

RFunk.failure(TypeError, "Expected a type of 'String' for return 'say_hello'")

Parameter Types

fun :say_hello => 'String -> String' do |name|
  pre {
    assert { name == RFunk.some('Bob') }
  }

  body {
    val return: "Hello #{name}!"
    value(:return)
  }

  post {
    value(:return) == RFunk.some('Hello Bob!')
  }
end

If the parameter type is not a string we would get the following error:

RFunk.failure(TypeError, "Expected a type of 'String' for parameter '1'")

Pattern Matching

You can also do some pattern matching

fun :something do
  match(RFunk.some('YES')) do |p|
    p.with :some, ->(v) { "#{v} IT WORKED" }
  end
end

Would return:

RFunk.some('YES IT WORKED')

To use a default match use the following:

fun :something do
  match(RFunk.some('YES')) do |p|
    p.with :_, ->(v) { "#{v} IT WORKED" }
  end
end

Would return:

RFunk.some('YES IT WORKED')

The only supported types are option and either.

Functions

As you saw in the above examples, you can define your own functions using the fun keyword. This keyword has aliases of fn, func and defn.

Pipeline

This is similar to the pipeline operator in Unix

RFunk.option({ a: 1 }).pipe { |h| h.to_s }.pipe { |s| "#{s}, hello" }

Would return

RFunk.some('{:a=>1}, hello')

Third party libraries

We have added the following dependencies:

This will allow you to use more functional concepts.

Contributing to rfunk

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
  • Fork the project.
  • Start a feature/bugfix branch.
  • Commit and push until you are happy with your contribution.
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright

Copyright (c) 2019 Alex Falkowski. See LICENSE.txt for further details.