Mayak
Overview
Mayak is a library which aims to provide abstractions for well typed programming in Ruby using Sorbet type checker. Mayak provides generic interfaces and utility classes for various applications, and as a foundation for other libraries.
Installation
In order to use the library, add the following line to your Gemfile
:
gem "mayak"
or install it via the following command:
gem install "mayak"
If you are using tapioca, add following line into tapioca's require.rb
before generating rbi's for the gem:
require "mayak"
Documentation
Mayak consists from separate classes and interfaces as well as separate modules for specific domains.
Caching
Monads
HTTP
Miscellaneous
Lazy
Lazy
classs represents a value that evaluates only during it's access, and evaluates only once
during the first access. Basically, Lazy
wraps a block of code (thunk) that returns a value (Lazy
has single type parameter of a value), and executes only when the value accessed for the first time
and then stores it afterward.
In order to build Lazy
a type parameter of value holded should be provided as well as a block that computes a value of the type.
Note that the block is not executed right away.
lazy1 = ::Mayak::Lazy[Integer].new { 1 }
buffer = []
lazy2 = ::Mayak::Lazy[Integer].new do
buffer << 1
1
end
buffer
#> []
To access the value call #value
. If the value is not yet computed, provided block will be executed and its result will be stored. Further invokations
of this method won't execute the block again.
buffer = []
lazy = ::Mayak::Lazy[Integer].new do
buffer << 1
1
end
buffer
#> []
# Will execute the block and return the computed value.
lazy.value
#> 1
buffer
#> [1]
# Will return the memoized value, but won't call the block again
lazy.value
#> 1
buffer
#> [1]
Lazy
can be used in situations, when we want to inject some dependency into some class or method, but it may not be used, and the computation or aacquisition of the dependency may be cosftul. In this cases, it's acquisitation may be wrapped in lazy.
In more imperative style
sig { params(env_variable: String, file_content: ::Mayak::Lazy[String], default: String).returns(String) }
def fetch_config(env_variable, file_content, default)
from_environment = ENV[env_variable]
if env.empty?
file_config = ::Core::Json.parse(file_content.value).success_or({})
from_file = file_config["configuration"]
if from_file.empty?
default
else
from_file
end
else
from_environment
end
end
Using Mayak monads:
include ::Mayak::Monads::Maybe::Mixin
sig { params(env_variable: String, file_content: ::Mayak::Lazy[String], default: String).returns(String) }
def fetch_config(env_variable, file_content, default)
Maybe(ENV[env_variable])
.recover_with_maybe(::Core::Json.parse(file_content.value).to_maybe)
.flat_map { |json| Maybe(json["configuration"]) }
.value_or(default)
end
This method receives name of environment variable, and file content as lazy value. The method
tries to read the environment variable, and if it's not present and reads the file content to find the configuration.
Lazy
allows to incapsulate behaviour of reading from file, so it can be passed as dependency, method #fetch_config
doesn't
know anything about reading from file, but because of usage of lazy
we can postpone it's execution thus avoiding unnecessary work.
Lazy
can be transformed via methods #map
and #flat_map
.
Method #map
allows to transform value inside Lazy
without triggering executing. Note that #map
returns
a new instance without mutating previous Lazy
.
int_lazy = ::Mayak::Lazy[Integer].new do
puts("On initialize")
1
end
string_lazy = int_lazy.map do |int|
puts("On mapping")
int.to_s
end
int_lazy.value # 1
#> On initialize
string_lazy.value # "1"
#> On initialize
#> On mapping
sig { params(file_content: ::Mayak::Lazy[String]).returns(::Mayak::Lazy[Maybe[String]]) }
def file_content_config(file_content)
file_content.map do |file_content|
::Core::Json
.parse(file_content.value)
.to_maybe
.flat_map { |json| Maybe(json["configuration"]) }
end
end
Method #flat_map
allows to chain lazy computations. It receives a block, that builds a new Lazy
value from the value of original
Lazy
and returns a new instance of Lazy
.
sig { params(env_name: String).returns(::Mayak::Lazy[String]) }
def lazy_env(env_name)
::Mayak::Lazy[String].new { ENV[env_name] }
end
env_variable_name = ::Mayak::Lazy[String].new { "VARIABLE" }
env_variable = env_variable_name.flat_map { |env_name| lazy_env(env_name) }
This may be useful when want to perform a lazy computation based on result of some other lazy computation without enforcing the evaluation.
For example we have a file that contains list of file names. We can build a lazy computation that read all lines from this code.
sig { params(file_name: String).returns(::Mayak::Lazy[T::Array[String]]) }
def read_file_lines(file_name)
::Mayak::Lazy[T::Array[String]].new { File.read(file_name).split }
end
Let's we want to read all filenames from the root file, and then read the first file lazily. In this cases, the lazy computation can be chained via #flat_map
:
sig { params(file_name: String).returns(::Mayak::Lazy[T::Array[String]]) }
def read_first_file(file_name)
read_file_lines(file_name).flat_map do |file_names|
Maybe(file_names.first)
.filter(&:empty?)
.map { |file| read_file_lines(file) }
.value_or(::Mayak::Lazy[T::Array[String]].new { [] })
end
end
In order to combine two lazies of different types into a single one, method #combine
can be used.
This method receives another lazy (it can be lazy of different type), and a block
and returns a lazy containing result of applying passed blocked to values calculated by lazies.
class ConfigFiles < T::Struct
const :database_config_file, ::File
const :server_config_file, ::File
end
sig { returns(::Mayak::Lazy[File]) }
def database_config_file
::Mayak::Lazy[File].new { File.new(DATABASE_CONFIG_FILE_NAME, "r") }
end
sig { returns(::Mayak::Lazy[File]) }
def server_config_file
::Mayak::Lazy[File].new { File.new(SERVER_CONFIG_FILE_NAME, "r") }
end
sig { returns(::Mayak::Lazy[ConfigFiles]) }
def config_files
database_config_file.combine(server_config_file) do |db_file, server_file|
ConfigFiles.new(
database_config_file: database_config_file,
server_config_file: server_file
)
end
end
The same behaviour can be achieved with a method .combine_two
:
sig { returns(::Mayak::Lazy[ConfigFiles]) }
def config_files
::Mayak::Lazy.combine_two(database_config_file, server_config_file) do |db_file, server_file|
ConfigFiles.new(
database_config_file: database_config_file,
server_config_file: server_file
)
end
end
There are also methods .combine_three
, .combine_four
upto .combine_sevel
to combine multiple lazies of diffent types.
If you need to combined multiple lazies containing the same value, you can use .combine_many
. It works
as Array#reduce
: receives an array of lazies containing the same type, initial value of result type, and a block
receiving accumulator value of result type, and value of next lazy.
sig { returns(::Mayak::Lazy[Integer]) }
def lazy
::Mayak::Lazy.combine_many(
[::Mayak::Lazy[Integer].new(1), ::Mayak::Lazy[Integer].new(2), ::Mayak::Lazy[Integer].new(3)],
0
) { |acc, value| acc + value }
end
lazy.value # 10
If you need to transform array of lazies of some value into lazy of array of the value, you can use .sequence
method.
sig { returns(::Mayak::Lazy[T::Array[Integer]]) }
def lazy
::Mayak::Lazy.sequence([::Mayak::Lazy[Integer].new(1), ::Mayak::Lazy[Integer].new(2), ::Mayak::Lazy[Integer].new(3)])
end
lazy.value # [1, 2, 3]
Function
In some situations Sorbet can not infer a type of proc passed:
sig {
type_parameters(:A)
.params(blk: T.proc.params(arg0: T.type_parameter(:A)).returns(T.type_parameter(:A)))
.returns(T.proc.params(arg0: T.type_parameter(:A)).returns(T.type_parameter(:A)))
}
def proc_identity(&blk)
blk
end
T.reveal_type(proc_identity { |a| 10 })
# This code is unreachable https://srb.help/7006
# proc_identity { |a| 10 }
Mayak::Fuction
allows explicitly define input and output types to help sorbet infer types:
sig {
type_parameters(:A)
.params(
fn: Mayak::Function[T.type_parameter(:A), T.type_parameter(:A)])
.returns(Mayak::Function[T.type_parameter(:A), T.type_parameter(:A)])
}
def fn_identity(fn)
fn
end
T.reveal_type(
fn_identity(Mayak::Function[Integer, Integer].new { |a| a })
)
# Revealed type: Mayak::Function[Integer, Integer]
JSON
JSON
module provides a type alias to encode JSON type:
JsonType = T.type_alias {
T.any(
T::Array[T.untyped],
T::Hash[T.untyped, T.untyped],
String,
Integer,
Float
)
}
and methods to safely parse JSON:
Mayak::Json.parse(%{ { "foo": 1} })
#<Mayak::Monads::Try::Success:0x00000001086c8398 @value={"foo"=>1}>
Mayak::Json.parse(%{ { "foo: 1} })
#<Mayak::Monads::Try::Failure:0x00000001085ea250 @failure=#<Mayak::Json::ParsingError: unexpected token at '{ "foo: 1} '>>
Numeric
Numeric
method provides method for safe parsing numerical values:
Mayak::Numeric.parse_float("0.1")
#<Mayak::Monads::Maybe::Some:0x000000010bbb4070 @value=0.1>
Mayak::Numeric.parse_float("0.1sdfs")
#<Mayak::Monads::Maybe::None:0x000000010bab3e50>
Mayak::Numeric.parse_integer("10")
#<Mayak::Monads::Maybe::Some:0x0000000108fcdb78 @value=10>
Mayak::Numeric.parse_integer("10qq")
#<Mayak::Monads::Maybe::None:0x000000010bbf64c0>
Mayak::Numeric.parse_decimal("100")
#<Mayak::Monads::Maybe::Some:0x000000010ba78968 @value=0.1e3>
Mayak::Numeric.parse_decimal("100dd")
#<Mayak::Monads::Maybe::None:0x000000010bb718b0>
Random
Utils for random number generating
#jittered
Adds random noise for a number within specified range
# Yield a random number from 100 to 105
Mayak::Random.jittered(100, jitter: 0.05)
# 101.53359412200601
Mayak::Random.jittered(100, jitter: 0.05)
# 103.59043964431787
WeakRef
Parameterized weak Reference class that allows a referenced object to be garbage-collected.
class Obj
end
value = Obj.new
value = Mayak::WeakRef[Obj].new(value)
value.deref
#<Mayak::Monads::Maybe::Some:0x0000000103e8fa90 @value=#<Obj:0x000000010721de48>>
GC.start
value.deref
#<Mayak::Monads::Maybe::None:0x000000010715f6f0>
# Not necessarily will be collected after only one GC cycle