docs: def join(self, experiment, genometric_predicate, output="LEFT", joinBy=None,
               refName="REF", expName="EXP", left_on=None, right_on=None):
          """
          *Wrapper of* ``JOIN``
  
          The JOIN operator takes in input two datasets, respectively known as anchor
          (the first/left one) and experiment (the second/right one) and returns a
          dataset of samples consisting of regions extracted from the operands 
          according to the specified condition (known as genometric predicate). 
          The number of generated output samples is the Cartesian product of the number 
          of samples in the anchor and in the experiment dataset (if no joinby close 
          if specified). The attributes (and their values) of the regions in the output 
          dataset are the union of the region attributes (with their values) in the input 
          datasets; homonymous attributes are disambiguated by prefixing their name with 
          their dataset name. The output metadata are the union of the input metadata, 
          with their attribute names prefixed with their input dataset name.
          
          :param experiment: an other GMQLDataset
          :param genometric_predicate: a list of Genometric atomic conditions. For an explanation of each of them
                                       go to the respective page.
          :param output: one of four different values that declare which region is given in output 
                         for each input pair of anchor and experiment regions satisfying the genometric predicate:
                         
                         * 'LEFT': outputs the anchor regions from the anchor dataset that satisfy the genometric predicate
                         * 'RIGHT': outputs the anchor regions from the experiment dataset that satisfy the genometric predicate
                         * 'INT': outputs the overlapping part (intersection) of the anchor and experiment regions that satisfy
                           the genometric predicate; if the intersection is empty, no output is produced
                         * 'CONTIG': outputs the concatenation between the anchor and experiment regions that satisfy the 
                           genometric predicate, i.e. the output region is defined as having left (right) coordinates 
                           equal to the minimum (maximum) of the corresponding coordinate values in the anchor and 
                           experiment regions satisfying the genometric predicate
  
  
          :param joinBy: list of metadata attributes
          :param refName: name that you want to assign to the reference dataset
          :param expName: name that you want to assign to the experiment dataset
          :param left_on: list of region fields of the reference on which the join must be performed
          :param right_on: list of region fields of the experiment on which the join must be performed
          :return: a new GMQLDataset
          
          An example of usage, in which we perform the join operation between Example_Dataset_1 and Example_Dataset_2
          specifying than we want to join the regions of the former with the first regions at a minimim distance of 120Kb
          of the latter and finally we want to output the regions of Example_Dataset_2 matching the criteria::
              
              import gmql as gl
  
              d1 = gl.get_example_dataset("Example_Dataset_1")
              d2 = gl.get_example_dataset("Example_Dataset_2")
  
              result_dataset = d1.join(experiment=d2,
                                       genometric_predicate=[gl.MD(1), gl.DGE(120000)],
                                       output="right")
          """
  
          if isinstance(experiment, GMQLDataset):
              other_idx = experiment.__index
          else:
              raise TypeError("experiment must be a GMQLDataset. "
                              "{} was provided".format(type(experiment)))
          if isinstance(genometric_predicate, list) and \
              all([isinstance(x, GenometricCondition) for x in genometric_predicate]):
              regionJoinCondition = self.opmng.getRegionJoinCondition(list(map(lambda x: x.get_gen_condition(),
                                                                               genometric_predicate)))
          else:
              raise TypeError("genometric_predicate must be a list og GenometricCondition. "
                              "{} was found".format(type(genometric_predicate)))
  
          if isinstance(output, str):
              regionBuilder = self.opmng.getRegionBuilderJoin(output)
          else:
              raise TypeError("output must be a string. "
                              "{} was provided".format(type(output)))
  
          if not isinstance(expName, str):
              raise TypeError("expName must be a string. {} was provided".format(type(expName)))
  
          if not isinstance(refName, str):
              raise TypeError("refName must be a string. {} was provided".format(type(expName)))
  
          if isinstance(joinBy, list) and \
              all([isinstance(x, str) for x in joinBy]):
              metaJoinCondition = Some(self.opmng.getMetaJoinCondition(joinBy))
          elif joinBy is None:
              metaJoinCondition = none()
          else:
              raise TypeError("joinBy must be a list of strings. "
                              "{} was found".format(type(joinBy)))
  
          left_on_exists = False
          left_on_len = 0
          if isinstance(left_on, list) and \
              all([isinstance(x, str) for x in left_on]):
              left_on_len = len(left_on)
              left_on = Some(left_on)
              left_on_exists = True
          elif left_on is None:
              left_on = none()
          else:
              raise TypeError("left_on must be a list of strings. "
                              "{} was provided".format(type(left_on)))
  
          if isinstance(right_on, list) and \
              all([isinstance(x, str)] for x in right_on) and \
              left_on_exists and len(right_on) == left_on_len:
              right_on = Some(right_on)
          elif right_on is None and not left_on_exists:
              right_on = none()
          else:
              raise TypeError("right_on must be a list of strings. "
                              "{} was provided".format(type(right_on)))
  
          new_index = self.opmng.join(self.__index, other_idx,
                                      metaJoinCondition, regionJoinCondition, regionBuilder,
                                      refName, expName, left_on, right_on)
          new_local_sources, new_remote_sources = self.__combine_sources(self, experiment)
          new_location = self.__combine_locations(self, experiment)
          return GMQLDataset(index=new_index, location=new_location,
                             local_sources=new_local_sources,
                             remote_sources=new_remote_sources, meta_profile=self.meta_profile)