Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qo 1.0.0 #23

Merged
merged 12 commits into from
Feb 16, 2019
Merged

Qo 1.0.0 #23

merged 12 commits into from
Feb 16, 2019

Conversation

baweaver
Copy link
Owner

@baweaver baweaver commented Feb 11, 2019

Qo 0.99.0

When did it change from 1.0.0 to 0.99? Qo is going to be renamed, and this will be the last version officially under the name Qo. We'll be discussing where it's going from here soon.

Description

Massive restructure of the API, would be interested in getting a few opinions on what I have so far here. Likely I'll be restructuring the Readme significantly to try and reconcile it down.

Changes

Major Additions

Destructuring

You may remember my article on Destructuring in Ruby:

https://medium.com/rubyinside/destructuring-in-ruby-9e9bd2be0360

The techniques covered in there have now been implemented in Qo as an optional addition:

Qo.case(Person.new('Foo', 42), destructure: true) { |m|
  m.when(name: /^F/) { |name, age| Person.new(name, age + 1) }
  m.else
}
# => Person('Foo', 43)

The following methods have a destructure keyword option, defaulting to false:

  • Qo.case
  • Qo.match
  • Qo.result_match
  • Qo.result_case

New Branch Properties

The new method of creating and extending branches comes with several new properties of some interest. These are documented in the code, but exposed here due to the scope of the change.

Several of these have been added with the express purpose of creating custom pattern matchers as mentioned below in the next section.

name: [String]

Name of the branch. This is what binds to the pattern match as a method,
meaning a name of where will result in calling it as m.where.

precondition: Any [Symbol, #===]

A precondition to the branch being considered true. This is done for
static conditions like a certain type or perhaps checking a tuple type
like [:ok, value].

If a Symbol is given, Qo will coerce it into a proc. This is done to
make a nicer shorthand for creating a branch.

extractor: IDENTITY [Proc, Symbol]

How to pull the value out of a target object when a branch matches before
calling the associated function. For a monadic type this might be something
like extracting the value before yielding to the given block.

If a Symbol is given, Qo will coerce it into a proc. This is done to
make a nicer shorthand for creating a branch.

destructure: false

Whether or not to destructure the given object before yielding to the
associated block. This means that the given block now places great
importance on the argument names, as they'll be used to extract values
from the associated object by that same method name, or key name in the
case of hashes.

default: false [Boolean]

Whether this branch is considered to be a default condition. This is
done to ensure that a branch runs last after all other conditions have
failed. An example of this would be an else branch.

Custom Pattern Matchers and Class Mixins

The big changes here are the ability to create your own custom branches and pattern matchers, as mentioned in the Readme.

# Technically Some and None don't exist yet, so we have to "cheat" instead
# of just saying `Some` for the precondition
#
# We start by defining two branches that match against a Some type and a None
# type, extracting the value on match before yielding to their associated
# functions
SomeBranch = Qo.create_branch(
  name:        'some',
  precondition: -> v { v.is_a?(Some) },
  extractor:    :value
)

NoneBranch = Qo.create_branch(
  name:        'none',
  precondition: -> v { v.is_a?(None) },
  extractor:    :value
)

# Now we create a new pattern matching class with those branches. Note that
# there's nothing stopping you from making as many branches as you want,
# except that it may get confusing after a while.
SomePatternMatch = Qo.create_pattern_match(branches: [
  SomeBranch,
  NoneBranch
])

class Some
  # There's also a provided mixin that gives an `unfold` method that
  # works exactly like a pattern match without having to use it explicitly
  include SomePatternMatch.mixin

  attr_reader :value

  def initialize(value)
    @value = value
  end

  def self.[](value)
    new(value)
  end

  def fmap(&fn)
    new_value = fn.call(value)
    new_value ? Some[new_value] : None[value]
  end
end

class None
  include SomePatternMatch.mixin

  attr_reader :value

  def initialize(value)
    @value = value
  end

  def self.[](value)
    new(value)
  end

  def fmap(&fn)
    None[value]
  end
end

# So now we can pattern match with `some` and `none` branches using the `unfold`
# method that was mixed into both types.
Some[1]
  .fmap { |v| v * 2 }
  .unfold { |m|
    m.some { |v| v + 100 }
    m.none { "OHNO!" }
  }
=> 102

Some[1]
  .fmap { |v| nil }
  .unfold { |m|
    m.some { |v| v + 100 }
    m.none { "OHNO!" }
  }
=> "OHNO!"

Public API Changes

There are no feature-breaking changes in the 1.0.0 release with the public api, but the internal API has been substantially changed. If you were relying on it for anything your code will break.

Additive

Multiple Matcher Classes

There's one additive change to the current public API: you can combine Array and Keyword style matchers without errors:

Qo[Integer, succ: 3]

Previously that would crash, in 1.0.0 it'll work just fine.

Identity Argument

The public identity function has changed:

# old
IDENTITY = -> v { v }

# new
IDENTITY = -> itself { itself }

This is done so destructuring will defaultly return the entire object.

Removed

Helpers have been removed (count_by and dig) as Xf takes care of most of those features.

Deprecations

The entire former private API should be considered deprecated and broken beyond use. As mentioned in the documentation, the public api (lib/qo/public_api) should be considered the gold standard to implement against.

This will continue to be the case going forward, as it will be a major version change to remove or change any contract to the public api.

Addressed Issues

Issues reconciled or fixed by this upgrade:

Won't Fix Issues

Issues deemed to be outside of the scope of Qo, mostly because they would break the current API too much to implement:

TODO Issues

The only issue slated on TODO at this point is to add more practical examples:

#4

I may consider switching this issue to target getting a better break-up of the documentation into a series of guides. Thoughts? I'll likely open a new issue on this later.

Additional Notes

This is a pretty big change, and one I've been thinking about for quite some time. Likely I'll be working on the documentation a bit more over the next week, but I'd love any additional ideas on the API and implementation details in the mean time.

Thanks for reading and joining me on this crazy pattern matching ride!

@baweaver

@baweaver baweaver added this to the 1.0.0 milestone Feb 11, 2019
qo.gemspec Outdated

spec.add_runtime_dependency "any", '0.1.0'
spec.add_runtime_dependency "parser"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to remove this. Was trying to add some black magic for a "compile" step, turns out it won't work. Might revisit this later with TraceSpy or TracePoint and write an article on that later. The short version of it is you can intercept block calls and likely get the argument values.

This will give two potential things: deep destructuring, and dynamic compilation. It'll probably be a later project for shenanigans and fun.

@@ -0,0 +1,126 @@
require 'spec_helper'
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May make some shared examples later, enough of these are similar it'd make sense.

spec/qo_spec.rb Outdated
@@ -55,7 +55,7 @@
expect(result).to eq(nil)
end

it 'can deconstruct array to array matches' do
it 'can destructure array to array matches' do
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too happy with find and replace, should put these back

@@ -0,0 +1,298 @@
module Qo
module Matchers
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined hash and array matchers back into one file. Slight speed hit, but easier to work with

.travis.yml Outdated
@@ -1,7 +1,7 @@
sudo: false
language: ruby
rvm:
- 2.3.7
- 2.4.4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2.4.5 is current I think.

README.md Outdated
def fmap(&fn) None[value] end
end

# So now we can pattern match with `some` and `none` branches using the `unfold`
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace unfold with match throughout, as per suggestion

@baweaver
Copy link
Owner Author

Switching down to 0.99.0 in preparations for a very interesting move :)

@baweaver baweaver merged commit 26e0b7a into master Feb 16, 2019
@baweaver baweaver deleted the v1/redefine_api branch February 16, 2019 23:53
@baweaver baweaver restored the v1/redefine_api branch February 16, 2019 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants