These are chat archives for dry-rb/chat

12th
Jul 2018
BrandonKlotz
@BrandonKlotz
Jul 12 2018 13:32
When you create a struct class is there a way to pass in access to a separate models method?
Let me know if I am thinking about that in the right way.
Chris Richards
@cmrichards
Jul 12 2018 13:46
This change from .value to .value_on for Failures seems strange
I always return Failure in my services with an object, e.g. return Failure(errors). How can I access the errors if I can no-longer use result.value ?
Chris Richards
@cmrichards
Jul 12 2018 13:52
Ok, i see: result.failure
So now I need to use result.value! for Success and result.failure for Failure. It doesn't seem as simple/consistent now
Grant Shangreaux
@gcentauri
Jul 12 2018 14:34
@BrandonKlotz do you have an example for what you are trying to do? generally, a struct class should just be for holding data, if you want to process that data with another class, pass the struct to it. i have added simple predicates to structs though, referring to constants from another module.
Jaromír Červenka
@Cervajz
Jul 12 2018 14:44
@cmrichards Or result.success :)
I like it more that having .value for both cases
Nikita Shilnikov
@flash-gordon
Jul 12 2018 14:56
if you're like .value you're probably don't need monads
it's unsafe to have a common method since it returns different types
Jaromír Červenka
@Cervajz
Jul 12 2018 15:09
@flash-gordon Not sure if that was meant to me - but that's what I was saying - I like the new naming much more as it is more clear and distincts between two opposite results :)
I like it more that having .value for both cases
that => than
Chris Richards
@cmrichards
Jul 12 2018 15:10
i konw waht you maent ;-)
@Cervajz result.sucess ok thats better
Nikita Shilnikov
@flash-gordon
Jul 12 2018 15:12
@Cervajz fatal typo :)
Jaromír Červenka
@Cervajz
Jul 12 2018 15:12
Yeah :)
Chris Richards
@cmrichards
Jul 12 2018 15:56
Ok I'm using result.success now, but it still doesn't seem as readable as result.value which makes it clear you are getting the value of the result.
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:08
this is because you're not supposed to call it, monads don't have interface for extracting value
value! is inherently unsafe
use value_or for folding
Jaromír Červenka
@Cervajz
Jul 12 2018 16:18
@flash-gordon What do you mean by "folding"?
And what would be proper usage of this then?
transaction = ::Sessions::AuthenticateUser.new.call(email: email, password: password)
transaction.success? ? transaction.success : GraphQL::ExecutionError.new(transaction.failure)
If we're not suppose to call .success on the transaction result
I am confused now :)
Piotr Solnica
@solnic
Jul 12 2018 16:21
shouldn't this use value_or instead? I thought success and failure are kinda...low-level, you're basically accessing result object's state like that
Jaromír Červenka
@Cervajz
Jul 12 2018 16:22
@solnic value_or is not available on Failure

So

transaction.success? ? transaction.value_or : GraphQL::ExecutionError.new(transaction.failure)

would be better?

Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:22
folding here is removing wrapper, i.e. going from Maybe[Int] to Int, Result[User, Error] to User, things like that. You can't do it OOTB because both Maybe and Result can have no value
and you need to provide a default one for such case
Jaromír Červenka
@Cervajz
Jul 12 2018 16:24
And is it applicable in the context of Transaction result? I always expect some result / value based on the transaction "success"
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:24
@Cervajz you don't need to unwrap monadic values until you have side effects
m.fmap { |success_value| render json: success_value }.or { |errors| render json: errors }
Jaromír Červenka
@Cervajz
Jul 12 2018 16:25
Uhm
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:26
let's take another attempt
what's the type of transaction.success?
Jaromír Červenka
@Cervajz
Jul 12 2018 16:26
Whatever actually, no? :)
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:26
doesn't work like this
Whatever isn't compatible with GraphQL::ExecutionError
Jaromír Červenka
@Cervajz
Jul 12 2018 16:27
I was looking for something to replace my "service classes", which can return true, model, hash, etc
But that's for failure
That's always hash
module Mutations
  module Sessions
    class AuthenticateUser < ApplicationResolver
      argument :email, String, required: true
      argument :password, String, required: true

      type Types::TemporaryTokenType, null: false

      def resolve(email:, password:)
        transaction = ::Sessions::AuthenticateUser.new.call(email: email, password: password)
        transaction.success? ? transaction.success : GraphQL::ExecutionError.new(transaction.failure)
      end
    end
  end
end
This is the whole thing
so in case of success I expect to get Types::TemporaryTokenType
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:28
the result of it is whatever or GraphQL::ExecutionError
what's the point of such a value?
how do you check it later?
with is_a? or what?
like is_a?(GraphQL::ExecutionError)?
Jaromír Červenka
@Cervajz
Jul 12 2018 16:30
No, in that case, the exception is caught by GQL library and rednered as json
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:30
ugh, so you have exceptions on top of this
but GraphQL::ExecutionError.new doesn't look like raising one
Jaromír Červenka
@Cervajz
Jul 12 2018 16:31
No
The resolve method terurn value is either Types::TemporaryTokenType or ExecutionError
Then the GQL resolver translates it to json output
the transaction.failure could contain errors from dry-validation
module Sessions
  class AuthenticateUser
    include Dry::Transaction

    step :validate_input
    step :verify_password
    step :check_lockdown
    step :check_user_restrictions
    step :generate_auth_token

    private

    def validate_input(input)
      validation = AuthenticateUserSchema.call input
      validation.success? ? Success(input) : Failure(validation.errors)
    end

...
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:34
ok, now see, the problem that either is what the Result monad does, ideally, you don't need to unwrap values since. Since that is_a? check sits in thah resolver and it's not compatible with monads it's safe to unwrap in this particular case
but only as a means for compatibility with some third-party code
Jaromír Červenka
@Cervajz
Jul 12 2018 16:35
Pardon my stupidity here - but if I make a step back
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:35
dry-validation has a monads extension btw
Jaromír Červenka
@Cervajz
Jul 12 2018 16:36
Isn't purpose of Transaction to return 1) indication of success/failure and 2) value itself ?
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:36
you can't unwrap value
safely
that's the point
dry-transaction has dry-matcher built-in for this reason
you don't unwrap the result, you match it
on every match you run a side effect
this is basic usage
Jaromír Červenka
@Cervajz
Jul 12 2018 16:39
Side effect - meaning then using it for json rendering, or whatever
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:39
yep
Jaromír Červenka
@Cervajz
Jul 12 2018 16:40
I am kind of confused by the words "unwrap safely" - can you elabolrate on that please?
I am looking at the dry-matcher docs
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:41
you can't assign "Types::TemporaryTokenType or ExecutionError" to one variable
they have different types
Jaromír Červenka
@Cervajz
Jul 12 2018 16:42
Ok, that I understand
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:42
the only thing in common they have is Object
which is useless
it only allows you to have is_a? check
Jaromír Červenka
@Cervajz
Jul 12 2018 16:42
Ok, still understand
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:43
and it's kind of silly to wrap everything in monads and give them up in the end
for no reason
ok, you have a reason
you showed :)
Jaromír Červenka
@Cervajz
Jul 12 2018 16:47

Clarly I have to study monads :) To be honest I just wanted to replace my messy service objects with something nicer, with more clear "DSL". As I had this before:

class ApplicationService
  private

  def error(errors = nil, data = nil)
    errors = errors.is_a?(ServiceResult) ? errors.errors : [errors].flatten
    ServiceResult.new(status: :error, errors: errors, data: data)
  end

  def success(data = nil)
    ServiceResult.new(status: :success, data: data)
  end
end
....
class ServiceResult
  attr_reader :status, :errors, :data

  def initialize(status:, errors: [], data: nil)
    @status = status.to_sym
    @errors = errors
    @data = data
  end

  def success?
    status == :success
  end

  def failed?
    !success?
  end

  def errors_to_str
    errors.join('. ')
  end

  def error
    errors.first
  end
end
....

# Example

class SendUserSMSService < ApplicationService
  def initialize(user:, message:)
    @user = user
    @message = message
  end

  def call
    return error('Phone cannot be empty') if user.phone.blank?

    sms = sms_client.new.send_sms(to: user.phone, message: message)
    sms.success? ? success : error(sms.errors)
  end
...
end

But that was messy. I like how transactions force me to make small steps with clear "left/right" result.

I like dry-transaction together with dry-validation
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:50
yeah, monads can help with this, you even can unwrap them but you should do it in a single safe place of your app. In some glue code before calling GQL or whatever you have
Jaromír Červenka
@Cervajz
Jul 12 2018 16:51
The thing is that every transaction returns different value (as object/type)
So one place to "unwrap them all" does not make sense, right?
Nikita Shilnikov
@flash-gordon
Jul 12 2018 16:52
yes, handle in a single place
you can also check out do notation from dry-monads, the idea is similar to dry-transaction (identical actually) but works without DSL http://dry-rb.org/gems/dry-monads/1.0/do-notation/
it can help you understand how it works
Jaromír Červenka
@Cervajz
Jul 12 2018 16:53
Will look at that, thank you